error handling

[errorHandling] spring, 실습하며 마주한 UnsatisfiedDependencyException -> 빈 주입이 실패하는 에러

기록하는 습관. 2023. 1. 11. 17:40

안녕하세요. 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에 값을 지정을 해주지 않으셔서 생겼던 문제로 보여집니다.