제어의 역전
의존성이란?
클래스 A가 클래스 B를 사용하고 있다면, A는 B에 의존적이다.
의존성 주입
이전에 실습했던 Service, Controller 코드를 보자.
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
}
@RestController
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
}
userRepository나 userService 객체를 직접 만들지 않았다. 그러나 IoC 컨테이너에서 이들을 Bean으로 관리하기에, 각 객체들을 DI 받아 사용할 수 있었던 것!
무엇을 Bean으로 쓰고, 무엇을
new로 만들지?
- 재사용되거나 교체 가능한 비즈니스 로직/인프라는 Bean으로 관리한다.
- Service, Controller, Repository, ...
- 데이터 객체(매번 새로운 상태를 가짐)는 개발자가 직접 관리한다.
- User, OrderRequest, ...
@Component가 붙은 클래스는 Bean으로 등록된다.@ComponentScan이 선언된 패키지를 기준으로 하위 패키지들을 탐색한다.@Component를 직접 붙일 수도 있지만, 다른 어노테이션에 이미 합쳐진 경우가 있다.@RestController는 @Controller와 @ResponseBody의 합성이었다.
@Component를 가지는 어노테이션들
@Component: 개발자가 직접 Bean으로 등록하려는 경우(클래스 레벨)@Controller(@RestController)@Service@Repository
@ComponentScan역시 이미 사용 중이었다!
@SpringBootApplication에@ComponentScan이 포함된다.
@Configuration을 붙여준다.@Bean을 붙여준다.@Bean이 붙은 메서드의 반환 객체가 Bean으로 등록된다.보통은 자동 등록을 훨씬 많이 사용한다.
주로 소스 코드를 직접 수정할 수 없는 외부 라이브러리 클래스를 Bean으로 등록할 때 수동 등록을 사용한다. (외부 클래스에 @Component를 붙일 수 없으니)
@Autowired 어노테이션을 붙여 사용한다. 크게 다음의 세 가지 방법이 있다.
이 중 생성자 주입만 사용한다. 이전까지 실습했던 방법과 같다.
@Service
public class MemberService {
private final MemberRepository memberRepository;
@Autowired // 생성자가 1개만 있으면 @Autowired 생략 가능
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
// Lombok의 @RequiredArgsConstructor를 사용할 수도 있다.
}
필드 주입과 세터 주입을 쓰면 필드를 final로 선언할 수 없다. 도중에 객체가 바뀔 수 있어 안전하지 않다. (불변성 보장이 깨진다)
안전하지 않은 코드
@Service
public class MemberService {
private MemberRepository memberRepository;
@Autowired
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
public void hello() {
// memberRepository에 할당된 Bean 대신 null을 할당
memberRepository = null;
}
}
@Component
public class KakaoPayService implements PayService { ... }
@Component
public class NaverPayService implements PayService { ... }
@Service
@RequiredArgsConstructor
public class OrderService {
// 카카오페이가 주입될까, 아니면 네이버페이가 주입될까?
private final PayService payService;
// ...
}
위의 경우 에러가 발생한다. 다음과 같이 우선순위를 지정해야 한다.
@Primary@Component
public class KakaoPayService implements PayService { ... }
@Primary // PayService 타입의 빈이 여러 개일 때, 우선권을 갖는다!
@Component
public class NaverPayService implements PayService { ... }
@RestCotroller
@RequiredArgsConstructor
public class PayController {
private final PayService payService; // NaverPayService가 자동으로 주입됨
// ...
}
@Qualifier@Component("kakaoPay") // "kakaoPay"라는 별명을 붙여줌
public class KakaoPayService implements PayService { ... }
@Component("naverPay")
public class NaverPayService implements PayService { ... }
@Service
public class OrderService {
private final PayService payService;
// "kakaoPay"라는 이름을 가진 빈을 주입해줘!
// 이 경우 생성자를 직접 작성해야 하니, @RequiredArgsConstructor 사용은 못함
public OrderService(@Qualifier("kakaoPay") PayService payService) {
this.payService = payService;
}
}
build.gradle에 다음 의존성을 추가해야 한다.
implementation 'org.springframework.boot:spring-boot-starter-validation'
주로 쓰는 Bean Validation
| 어노테이션 | 의미 |
|---|---|
@NotBlank | 공백이 아닌 문자 1개 이상 (null, "", " " 모두 거부) |
@Email | @가 포함된 이메일 형식 |
@Size(min=a, max=b) | 문자열 길이, 컬렉션 크기 제한 |
@Min(a)/@Max(b) | 숫자 최소/최대값 |
@Pattern(regexp=str) | 정규표현식 패턴 (전화번호, 주민번호 형식 등) |
message를 사용하여 커스텀 에러 메시지 설정 가능
// 직접 작성
@NotBlank(message = "이름을 입력해주세요!")
private String name;
// 다른 옵션값 포함 가능
@Size(min = 8, message = "비밀번호는 {min}자 이상이어야 합니다")
private String password;
DTO에는 Validation 규칙을, Controller에는 @Valid를 붙여주면 자동으로 검증한다.
| DTO | Controller |
|---|---|
이메일 형식을 틀리게 해서 요청을 보내니, 검증에 통과하지 못해 실패했다.