- 생성자를 통해서 의존관계 주입
- 수정자 주입(setter를 통한 주입)
- 필드에 바로 주입
- 일반 (아무) 메소드에다가 주입
: 생성자를 통해 의존관계 주입 (지금껏 사용했던 방법)
⭐️ 특징
즉, 로미오와 줄리엣👩🏼❤️👨🏼 공연에 비유하자면 공연 도중 배우를 바꾸고 싶지 않은 것. 공연 전 배우를 모두 정해놓고 공연을 시작 !!
(웬만해서는 생성자에는 모두 값을 채워주어야 함 !! ➡️ 필수)
📌 OrderServiceImpl.java
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired // 이 경우 생략 가능(생성자가 하나라)
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
...
➡️ 이처럼 생성자가 ⭐️ 딱 하나인 경우는 @Autowired
를 생략해도 자동 주입. (물론 스프링 빈에만 해당)
: setter (필드의 값을 변경하는 수정자 메소드)를 통해 의존관계를 주입하는 방법
( 참고: 스프링 컨테이너의 사이클 1. 스프링 빈 등록 2. 연관관계 자동 주입(@Autowired
) → (순서는 생성자, 수정자(setter)) )
⭐️ 특징
📌 (참고): 자바빈 프로퍼티는 필드의 값을 직접 변경하지 않고, setXxx, getXxx라는 메소드를 통해 값을 읽고 변경하도록 하는 규칙 ➡️ 자바빈 프로퍼티 규약
class Data {
private int day;
public void setDay(int day) {
this.day = day;
}
public int getDay() {
return day;
}
}
👽: 그냥 쓰지 마 !! (권장하지 않는 방법 ^^)
(어차피 setter를 또 만들어야됨. 그럼 이걸 왜 써?)
필드 주입 방법은 이름 그대~로 필드에 바~로 주입하는 방법이다. (아래와 같이)
@Autowired private MemberRepository memberRepository;
⭐️ 특징
@Autowired
는 순수한 자바 코드로만 이루어진 테스트에서 동작하지 않는다. @SpringBootTest
처럼 스프링 컨테이너를 테스트에 통합한 경우에만 가능.)단, 사용해도 되는 곳이 있긴하다.🤷🏻
@Configuration
같은 곳에서만 특별한 용도로 사용⭐️ 특징
📌 (참고): 의존관계 자동 주입은 스프링 컨테이너가 관리하는 스프링 빈이어야 동작.
스프링 빈이 아닌 (ex.Member) 클래스에서 @Autowired
코드를 적용하면 아무런 기능이 없다.
주입할 스프링 빈이 없어도 동작해야 할 경우는 있다.
이때 @Autowired
(required 옵션 default값이 true이기 때문에)만 사용한다면 자동 주입 대상이 없을 때 오류가 발생한다.
📌 해결방법
@Autowired(required=false)
: 자동 주입할 대상이 없으면 수정자 메소드 자체가 호출 안 됨org.springframework.lang.@Nullable
: 자동 주입할 대상이 없으면 null이 입력Optional<>
: 자동 주입할 대상이 없으면 Optional.empty가 입력 (값이 있을 수도 있고 null일 수도 있는 상태를 감싼 것)
✔️ test/hello.core에 autowired 패키지 생성 후 AutowiredTest.java
package hello.core.autowired;
import hello.core.member.Member;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.lang.Nullable;
import java.util.Optional;
public class AutowiredTest {
@Test
void AutowiredOption() {
// 이렇게 TestBean을 넣으면 스프링 빈으로 등록 됨.
ApplicationContext ac = new AnnotationConfigApplicationContext(TestBean.class);
}
static class TestBean {
// 그냥 @Autowired로만 했을 때 기본값이 true이기 때문에 에러 가 남.
// Member가 스프링 빈으로 등록되는 것이 아니기 때문
@Autowired(required = false)
public void setNoBean1(Member noBean1) {
System.out.println("noBean1 = " + noBean1);
}
@Autowired
public void setNoBean2(@Nullable Member noBean2) {
System.out.println("noBean2 = " + noBean2);
}
@Autowired
public void setNoBean2(Optional<Member> noBean3) {
System.out.println("noBean3 = " + noBean3);
}
}
}
(Member는 스프링 빈이 아님)
📌 (참고):
@Nullable
,Optional
은 스프링 전반에 걸쳐 지원됨. 예를 들어 생성자 자동 주입에서 특정 필드에만 사용해도 됨.
😺: 생성자 주입이 좋다~ 좋다~ 하는데 왜 생성자 주입이 좋다는 걸까?
대부분의 의존관계 주입은 한 번 발생하면 애플리케이션 종료 시점까지 의존관계가 변경될 일이 없다. 오히려 대부분의 의존관계는 애플리케이션 종료 전까지 변하면 안 된 다 !! ➡️ 불변
수정자 주입을 사용하면 어차피 setXxx 메소드를 public으로 열어두어야 하고
이렇게 열어두면 누군가 실수로 변경해버릴 수도 있다.
(변경하면 안 되는 메소드를 이렇게 열어두는 것은 좋은 설계 방법이 아니다.)
생성자 주입은 객체를 생성할 때 딱 한 번만 호출되므로 이후 호출되는 일이 없어 불변하게 설계가 가능하다.
프레임워크 없이 순수 자바 코드만으로 단위 테스트를 하는 경우는 다분하다.
수정자 의존관계의 경우 @Autowired
가 프레임워크 안에서 동작할 때는 의존관계가 없으면 오류가 발생하지만 프레임워크 없이 순수 자바 코드 단위 테스트에서는 실행은 가능하다.
하지만 NPE(Null Point Exception)이 발생하는데 이는 의존관계 주입이 누락 되었기 떄문이다.
✔️ 생성자 주입을 사용하면 주입 데이터를 누락했을 때 컴파일 오류가 발생하며 IDE 에서 어떤 값을 필수로 주입해야 하는지 바로 알려준다.
생성자 주입을 사용하면 필드에 final키워드를 사용할 수 있다. 이는 값이 꼬옥 설정되어야 하는데 혹시라도 생성자에서 값이 설정되지 않는 오류를 컴파일 시점에 막아준다.
java: variable discountPolicy might not have been initialized
👽: 컴파일 오류는 세상에서 가장 빠르고 좋은 오류입니다 !!
📌 생성자 주입을 제외한 나머지 주입 방식은 모두 생성자 이후 호출되므로 필드에
final
키워드를 사용할 수 없다. 오직 생성자 주입 방식만final
키워드 사용 가능
생성자주입
을 사용한다고 생각 !!수정자주입
.필드주입
은 ...위에서 계속, 몇번이나 필드 주입은 쓰지 말라고 했다. 근데 너무 편하지 않은가? 간단하고,,
개발 시 불변이 대부분이라 생성자에 final 키워드를 많이 사용한다. 그런데 생성자도 만들고, 주입 받은 값을 대입하는 코드도 만들고,, 이런걸 생각하면 자꾸 필드 주입에 손이 간다. 🦐
이러다 필드 주입을 사용해버리기 전에 코드를 최적화하는 방법을 알아보자.
✔️ 우선 우리가 쓰던 원래 코드의 일부 OrderServiceImpl.java
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired // 이 경우 생략 가능(생성자가 하나라)
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
그런데 생성자가 하나만 있으므로 @Autowired
를 일단 생략
(한 줄 줄였다 ^ ^)
그리고 롬복 라이브러리를 적용하자.
✔️ build.gradle에 내용 추가
plugins {
id 'org.springframework.boot' version '2.5.3'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
group = 'hello'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
// lombok 설정 추가 시작
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
// lombok 설정 추가 끝
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
testImplementation ('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
// lombok 라이브러리 추가 시작
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombkt'
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
// lombok 라이브러리 추가 끝
}
test {
useJUnitPlatform()
}
➡️ 그 다음 코끼리 꼬옥 눌러주기 (아니면 아래처럼)
⬆️ (mac기준) preferences에서 plugins에서 lombok 설치
⬆️ 노란색 화살표 꼭꼭 체크 !!
그리고 이제 아무 클래스나 생성해서 setter, getter를 확인해보자.
✔️ HelloLombok.java
package hello.core;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
// 이렇게 해두면 setter, getter 자동으로 만들어줌
// (원래는 우리가 setter, getter 코드를 생성했었음 -> 코드를 확 줄여줌)
public class HelloLombok {
private String name;
private int age;
public static void main(String[] args) {
HelloLombok helloLombok = new HelloLombok();
helloLombok.setName("dbsrud");
String name = helloLombok.getName();
System.out.println("name = " + name);
}
}
➡️ 오~~ 이전까지는 setter, getter 코드를 일부러 생성했는데 그런 번거로움 없이도 setter, getter를 사용할 수 있게 됐다 !!
이제 롬복 라이브러리를 적용 시켰으니 다시 코드로 돌아가 최적화 시켜보자.
✔️ OrderServiceImpl.java
package hello.core.order;
import hello.core.discount.DiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
// 롬복을 사용함으로써 이 코드가 필요없어졌다 !! (오히려 필드 주입보다 깔끔해졌다.)
// @Autowired // 이 경우 생략 가능(생성자가 하나라)
// public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
// this.memberRepository = memberRepository;
// this.discountPolicy = discountPolicy;
// }
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
// 할인 정책은 오직 discountPolicy에게 맡기고 있기 때문에 설계가 잘 된 케이스
int discountPrice = discountPolicy.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
➡️ 롬복 라이브러리가 제공하는 @RequiredArgsConstructor
기능을 사용하면 final이 붙은 필드를 모아 생성자를 자동 생성해준다. (보이진 않음)
실제로 클래스 안 코드는
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
이렇게만 남은 것 !! 기능은 동일하지만 롬복이 자바의 애노테이션 프로세서 기능을 이용해 컴파일 시점에 생성자 코드를 자동으로 생성해준다. 실제 class를 열어보면 코드를 확인할 수 있다.
📌 최근에는 많이들 생성자를 딱 하나만 두고
@Autowired
를 생략한다.또한 Lombok 라이브러리의
@RequiredArgsConstructor
를 사용해 기능은 다 제공받되, 코드는 깔끔하게 사용한다.
내용이 길어 2로 이어쓰겠습니다.