안녕하세요. Java백엔드 개발자를 꿈꾸며 공부중인 김은철입니다.
해당 포스팅의 내용은 이해한 내용을 토대로 작성되었습니다.
그렇기 때문에 포스팅에 잘못된 내용이 포함되어 있을 수 있음과 미흡한 점이 있을 수 있습니다.
의아한 내용이나 틀린 내용이 있다면 적극적인 지적과 므흡한 내용, 그리고 추가적으로 학습하며 좋을 내용을 알려주시면 감사하겠습니다.
여러분의 지적과 관심은 저의 성장에 큰 도움이 될 것이라 확신합니다.
감사합니다.
@Transactional의 학습과 더불어 포스팅에 사용할 자료를 만들면서 접한 UnsatisfiedDependencyException
에러를 소개합니다.
* UnsatisfiedDependencyException 발생과 해결
1. 문제발생
- 기타 소스코드, 보지 않으셔도 됩니다.
@Getter
@NoArgsConstructor
@Entity
public class Member {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String hobby;
@Builder
Member(String name, String hobby) {
this.name = name;
this.hobby = hobby;
}
}
/ * -------------------------------- */
@RestController
@RequestMapping("/members")
@AllArgsConstructor
public class MemberController {
private MemberService memberService;
private MemberMapper mapper;
@PostMapping("creates")
public ResponseEntity createdMember(@RequestBody @Valid MembersCreatedRequestDto membersCreatedRequestDto){
List<Member> member = memberService.createdMembers(mapper.membersCreatedDtoToMembers(membersCreatedRequestDto));
return new ResponseEntity<>(HttpStatus.CREATED);
}
}
/ * -------------------------------- */
@Getter
public class MembersCreatedRequestDto {
@Valid
private List<MemberCreatedRequestDto> data;
}
/ * -------------------------------- */
@Getter
public class MemberCreatedRequestDto {
@NotBlank
private String name;
@NotBlank
private String hobby;
}
/ * -------------------------------- */
@Service
@AllArgsConstructor
public class MemberService {
private MemberRepository memberRepository;
public List<Member> createdMembers(List<Member> members) {
return (List<Member>)memberRepository.saveAll(members);
}
}
- **문제 발생지점 : MemberMapper.java **
@Component // [1]
public interface MemberMapper {
default List<Member> membersCreatedDtoToMembers(MembersCreatedRequestDto membersCreatedDto) {
List<Member> response = new ArrayList<>();
List<MemberCreatedRequestDto> memberCreatedRequestDtos = Optional.ofNullable(membersCreatedDto)
.map(MembersCreatedRequestDto::getData)
.orElseThrow(RuntimeException::new)
.stream().collect(Collectors.toList());
for(MemberCreatedRequestDto memberCreatedRequestDto : memberCreatedRequestDtos) {
Member member = Member.builder()
.name(memberCreatedRequestDto.getName())
.hubby(memberCreatedRequestDto.getHobby())
.build();
response.add(member);
}
return response;
}
}
문제 발생 지점은 MemberMapper.java였습니다. 코드를 보고 이해하실 필요는 없으며, @Component
만 봐주시면 되겠습니다. 해당 코드는 MemberController에서 MemberMapper를 사용하게됩니다. MemberMapper를 생성자 주입을 통해 의존 관계를 맺어주었고, 생성자 주입을 해주기 위해서는 Bean으로 등록해야하기 때문에 @ComponentScan이 읽을 수 있는 @Component를 작성해주었습니다. 그리고 빌드를 해보니 아래 로그와 함께 UnsatisfiedDependencyException가 발생하였습니다.
// 중간 로그 생략
// 중간 로그 생략
// 중간 로그 생략
2023-01-11 16:43:48.934 WARN 69700 --- [ restartedMain] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'memberController' defined in file ]
[ 앞 파일명 생략 \com\euncheol\springnote\member\controller\MemberController.class]: Unsatisfied dependency expressed through constructor parameter 1; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.euncheol.springnote.member.mapper.MemberMapper' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
// 중간 로그 생략
// 중간 로그 생략
// 중간 로그 생략
***************************
APPLICATION FAILED TO START
***************************
Description:
Parameter 1 of constructor in com.euncheol.springnote.member.controller.MemberController required a bean of type 'com.euncheol.springnote.member.mapper.MemberMapper' that could not be found.
Action:
Consider defining a bean of type 'com.euncheol.springnote.member.mapper.MemberMapper' in your configuration.
Description부분을 읽어보면 MemberMapper의 빈 타입을 찾을 수 없어서 생성자 주입이 되지 않았다고 알 수 있는데요. 왜 이런 문제가 발생하였을까요? 문제는 선언한 파일의 형태가 interface였기 때문입니다. 처음에 interface로 선언한 이유는 개발 습관에 있었습니다. 평소 학습을 하며 코드를 작성할 때 편의성을 위해 Mapstruct를 종속받아 런타임 시간에 Mapstruct 구현체를 만들어 종종 사용하고 했는데 Mapstruct를 종속성 추가 및 @Mapper 어노테이션을 선언하지 않아 생겼던 문제였습니다. 이 문제를 해결하기 위해 아래와 같은 조치를 취하고 실행하니 문제가 해결되었습니다.
2. 해결
- build.gradle
...
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
// 추가
implementation 'org.mapstruct:mapstruct:1.5.1.Final'
// 추가
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.1.Final'
}
...
- MemberMapper.java
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface MemberMapper {
default List<Member> membersCreatedDtoToMembers(MembersCreatedRequestDto membersCreatedDto) {
List<Member> response = new ArrayList<>();
List<MemberCreatedRequestDto> memberCreatedRequestDtos = Optional.ofNullable(membersCreatedDto)
.map(MembersCreatedRequestDto::getData)
.orElseThrow(RuntimeException::new)
.stream().collect(Collectors.toList());
for(MemberCreatedRequestDto memberCreatedRequestDto : memberCreatedRequestDtos) {
Member member = Member.builder()
.name(memberCreatedRequestDto.getName())
.hubby(memberCreatedRequestDto.getHobby())
.build();
response.add(member);
}
return response;
}
}
* 마무리하며
DOLOLACK님께서도 UnsatisfiedDependencyException에 대해서 포스팅해주셨습니다.
저와는 달리, 어노테이션을 통해 properties에 미리 선언한 속성값을 주입 받으려고 하셨으나 properties에 값을 지정을 해주지 않으셔서 생겼던 문제로 보여집니다.