생성자 주입 방식도 이렇게 두가지로 쓰일 수 있다.
1. public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider)로 설정하는 방법은
직접 작성한 생성자 주입 방법.
public class JwtAuthenticationFilter extends GenericFilterBean {
JwtTokenProvider jwtTokenProvider;
public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
}
}
2. private final JwtTokenProvider jwtTokenProvider;와 @RequiredArgsConstructor를 사용하는 방법은
롬복 라이브러리를 활용하여 생성자 주입을 간결하게 만드는 방법.
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends GenericFilterBean {
private final JwtTokenProvider jwtTokenProvider;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
}
}
위 두가지 방법 모두 생성자 주입 방식을 사용하며,
기능적으로 동일하다.
그리고 이렇게 JwtTokenProvider 클래스를 객체로 다른 클래스에서 사용할 수 있으려면
스프링 컨테이너에 빈으로 등록이 되어야한다.
따라서, JwtTokenProvider 클래스 상단에 @Component 어노테이션을 사용하거나,
수동으로 Config클래스를 따로 만들어서 그 클래스의 상단에 @Configuration 어노테이션과 JwtTokenProvider클래스의 빈 생성 메서드를 만드는 방법으로 빈 등록을 해주면 된다.
생성자 주입 방식을 사용하면,
테스트 코드에서 의존성을 쉽게 주입할 수 있고
불변성을 보장하는 장점이 있다.
@Autowired를 쓰면서도 왜 쓰는지 뭔지 잘 몰랐고 이제는 많이 안쓰인다고 해서 관심없었다.
의존성 주입을 위해 생성자 주입 방식만 알고 있으면 충분하다고 생각했기 때문이다.
근데 @Autowired를 정확히는 알고 넘어가야할 것 같아서 정리했다.
@Autowired는 의존성 주입(DI)를 위한 어노테이션이다.
아직까지도 많은 프로젝트와 코드에 사용되고 있다.
그러나 최근의 스프링 관례와 추세를 보면 생성자 주입 방식을 권장하고 있기 때문에,
생성자 주입을 사용하는 경우 @Autowired를 생략하는 것이 더 깔끔하고 명확하다는 의견이 있다고한다.
@Autowired를 사용하는 경우
1. 필드 주입 :
직접 클래스의 필드에 @Autowired를 붙여서 사용한다.
필드 주입은 간단하게 작성할 수 있지만, 변경 불가능한 불변성을 가질 수 없고,
테스트하기 어렵다는 단점이 있다.
@Autowired
private JwtTokenProvider jwtTokenProvider;
2. 세터(수정자) 주입 :
세터 메서드에 @Autowired를 붙여서 사용한다.
세터 주입은 선택적인 의존성 주입을 할 때 유용하다고 생각할 수 있지만,
생성 이후에도 의존성을 변경할 수 있다는 불변성 문제가 있다.
final로 의존성을 주입 받을 객체들을 설정하지 않고
setter를 열어놓고 setter로 의존성 주입을 받기 때문에 불변성 문제가 있는 것
private JwtTokenProvider jwtTokenProvider;
@Autowired
public void setJwtTokenProvider(JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}
3. 생성자 주입 :
생성자에 @Autowired를 붙여서 사용한다. 하지만 스프링 4.3이후부터는 단일 생성자인 경우 생략이 가능하다.
스프링에서는 생성자 주입을 권장하는 이유로, 불변성과 테스트 용이성 등의 장점이 있다. 그래서 많은 개발자들이 생성자 주입 방식을 선호하게 되면서,
단일 생성자의 경우 @Autowired를 생략하게 된 것이다.
private final JwtTokenProvider jwtTokenProvider;
@Autowired
public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}
* 필드 주입과 세터 주입은 왜 @Autowired를 붙여야하나?
필드랑 메서드라서 생성자인지 모르니까 그런 것이다.
@Autowired 어노테이션은 스프링 DI 매커니즘에서 어떤 필드나 메서드에 의존성 주입이 필요한지를 나타내기 위해 사용된다.
<생성자 주입 방식의 장점>
1. 순환 참조 방지
2. final 선언이 가능 ( 객체 불변성)
3. 테스트 코드 작성 용이
1. 순환 참조 방지
생성자 주입 방식은 순환 참조를 방지할 수 있다.
생성자 주입 방식은 필드 주입이나 세터 주입과는 빈을 주입하는 순서가 다르다.
필드 주입과 수정자 주입은 먼저 빈을 생성한 후,
주입하려는 빈을 찾아 주입한다.
생성자 주입은 먼저 생성자의 인자에 사용되는 빈을 찾거나 빈 팩토리에서 만든다.
그 후, 찾은 인자 빈으로 주입하려는 빈의 생성자를 호출한다.
즉, 먼저 빈을 생성하지 않고 주입하려는 빈을 먼저 찾는다.
따라서 생성자 주입 방식을 사용하면 순환 참조 자체가
프로젝트 실행하면서 바로 문제가 된다.
객체 생성 시점에 빈을 주입하기 때문에
서로 참조하는 객체가 생성되지 않은 상태에서 그 빈을 참조하기 때문에 오류가 발생하는 것이다.
사전에 순환 참조를 방지하기 위해서는 생성자 주입을 사용하는 것이 좋다.
2. final 선언이 가능 ( 객체 불변성)
필드 주입과 세터 주입은 필드를 final로 선언할 수 없다. 왜?
생성자를 통해서만 의존관계가 주입이되고,
외부에서 어느 누구도 생성자의 파라미터 값을 수정할 수가 없다.
3. 테스트 코드 작성 용이
스프링 컨테이너 도움 없이 테스트 코드를 더 편리하게 작성할 수 있다.
<불변성과 테스트 용이>
불변성과 테스트 용이성은 @Autowired의 사용 여부와는 직접적인 관련이 없다고 한다.
주입 방식과 클래스의 설계에 따라 결정된다고한다.
객체의 불변성
대체 불변성이 뭔지 이해가 쉽지 않았다.
불변성을 보장할 수 있는 방식인 생성자 주입 방식을
의존성 주입 방식에서 가장 좋은 방식이라고 설명하는데...
불변성 :
생성자 주입을 사용하면, 해당 의존성이 한 번 설정되면 변경할 수 없기 때문에 불변성을 보장받을 수 있다.
이는 필드가 final로 선언되어 있을 때 특히 그렇다.
생성자에서 의존성을 주입받고, 해당 필드를 변경할 수 없게 만들면 불변성을 보장할 수 있다.
Q :
불변성을 보장할 수 없다는 게 무슨 말일까?
프로젝트를 실행하게 되면, 어차피 모든 객체는 바꿀 수 없는 것 아닌가?
그럼 다 불변성 보장이 되는 것 아닌가?
A :
불변성을 말할 때,
객체 지향 프로그래밍과 디자인 원칙에서 '불변성'이란 객체가 생성된 후 그 상태를 변경할 수 없다는 것을 의미한다.
이것은 해당 객체가 안전하게 공유되고 사용될 수 있음을 의미하며,
이러한 특징은 객체의 생명 주기 동안 일관성을 보장하고, 예기치 않은 부작용을 예방하는 데 도움이 된다.
스프링의 컨텍스트 안에서의 불변성과는 약간 다른 개념이다. 스프링엥서 싱글톤 빈은 일단 생성되면 변경되지 않아야 한다. 그러나 필드 주입과 세터 주입을 사용하면 해당 빈의 특정 필드나 메서드를 외부에서 변경할 수 있게 된다. 이러한 변경 가능성은 객체의 불변성을 깨트리는 것이다.
예를들어
필드 주입을 사용하면 해당 필드에 public 또는 protected 접근 제어자를 사용하면 외부에서 필드 값을 변경할 수 있게 된다.
세터 주입을 사용하면 해당 세터 메서드를 통해 언제든지 의존성을 바꿀 수 있다.
반면, 생성자 주입을 사용하면
의존성은 생성자에서 한 번만 주입되며,그 후에는 변경되지 않는다. 따라서 생성자 주입은 불변성을 보장한다.
프로젝트를 실행할 때 모든 객체가 바뀌지 않는다는 것과 불변성이 보장된다는 것은 다르다.
바뀌지 않는다는 것은 실행 중에 값이 예기치 않게 변경되지 않는다는 것을 의미하고,
불변성이 보장된다는 것은 객체의 설계 단계에서 그 객체의 상태가 변경될 수 없도록 설계되었다는 것을 의미한다.
Q :
객체의 상태가 변경된다는 건 무슨 의미야?
하나의 클래스에서 메서드의 로직이 변경되면
해당 클래스로 만들어진 객체의 상태가 변경되었다는 걸 의미해?
그럼 프로젝트 실행전에 이렇게 메서드의 로직을 바꾼다면,
필드 주입이든 세터 주입이든 생성자 주입이든
다 객체의 상태가 변경되었다는 걸 의미하는 거 아니야?
그럼 어떤 의존성 주입 방식이든 불변성은 존재하지 않는데?
A :
객체의 '상태'는 객체 내부의 데이터나 속성, 즉 필드의 현재 값들을 의미한다.
객체의 '행동'은 그 객체의 메서드에 의해 나타나는 동작이나 기능을 의미한다.
객체의 상태가 변경된다는 것은 객체의 필드 값이 변경된다는 것을 의미한다.
메서드의 로직이 변경되면, 객체의 '행동'이 변경된 것이지, '상태'가 변경된 것은 아니다.
불변성은 객체가 생성된 후에 그 객체의 상태(필드 값)이 변경될 수 없음을 의미한다.
예를들어 다음과 같은 클래스가 있다.
public class User {
private final String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public void setAge(int age) {
this.age = age;
}
}
위의 User 클래스에서 name 필드는 불변의 상태를 가지고 있다. 왜냐하면 name 필드는 final로 선언되었기 때문에
한번 설정되면 변경될 수 없다.
반면에 age 필드는 세터 메서드를 통해 변경될 수 있으므로 불변의 상태를 가지고 있지 않다.
의존성 주입과 관련하여 불변성을 얘기할 때
생성자 주입 : 의존성이 한 번 주입되면 그 이후에는 변경할 수 없으므로 불변성이 보장된다.
세터 주입 : 세터 메서드를 통해 여러 번 의존성을 주입하거나 변경할 수 있다. 따라서 불변성이 보장되지 않는다.
필드 주입 : 직접 필드에 접근하여 값을 변경할 수 있다. (특히 접근제어자가 public이거나 protected인 경우).
따라서 불변성이 보장되지 않는다.
이렇게 불변성은 객체의 상태가 외부에 의해 변경될 수 있는지 없는지에 대한 것이다. 프로젝트 실행 전에 코드를 변경하는 것은 코드 변경이며 이것은 불변성과는 다른 문제이다.
Q :
실제 생성자를 통해서 실행시킬 때, name값을 넣잖아?
그때는 String으로 생긴 어떠한 값을 넣어도 되는거잖아?
그럼 name은 값이 변경이 되는건대?
불변의 상태가 아니지 않아?
A:
final 키워드와 불변성에 대해 좀 더 명확히 설명해보자면
final 키워드는 변수가 한 번 초기화되면 다시 값을 할당받을 수 없다는 것을 의미한다.
즉, 참조변수가 한 번 지정된 객체를 참조하면 다른 객체를 참조하도록 변경할 수 없다.
---------------------------------------------------------------------------------------------------------------------------------------------------------
<위 문장에 대한 추가 설명>
final 키워드는 변수가 처음으로 값을 갖게 되는 초기화가 한 번 되면 다시 값을 할당받을 수 없다 즉, 다른 값으로 변경할 수 없다.
* 클래스의 필드는 생성자에서 초기화될 수 있다.
객체를 가리키는 변수인 참조변수가 final로 선언되어 있을 때, 그 변수가 한 번 특정 객체를 참조하게 되면,
그 변수를 다른 객체를 참조하도록 바꿀 수 없다.
예시
public class User {
private final String name;
public User(String name) {
this.name = name;
}
}
public class Main {
User user = new User("David");
}
참조변수는 스택 영역에 있는 변수이고
지정된 객체는 힙 영역에 있는 객체인 것인가?
위와 같은 코드 예시가 있다고 하면
생성자를 통해 객체를 생성하면
User user = new User("David");에서
user가 참조변수이고,
지정된 객체가 new User("David");이다.
그럼 user는 스택 영역에 저장되고
지정된 객체인 new User("David");는 힙 영역에 저장되서
user가 힙 영역에 있는 지정된 객체를 가리키고 있는 상황이 만들어진다.
참조변수 : user와 같은 참조변수는 스택 영역에 저장된다.
이 참조변수는 객체의 메모리 주소를 가리키거나 참조한다.
객체 : new User("David");와 같은 객체는 힙 영역에 저장된다. 힙 영역은 객체의 실제 데이터와 그와 관련된 메서드 등을 저장하는 영역이다.
final 키워드 : private final String name;에서
final 키워드는 이 변수가 한 번 초기화(생성자 내에서나 직접 초기화 시) 되면 그 후에는 다른 값을 할당받을 수 없다는 것을 의미한다.
위 예시에서 User user = new User("David");
코드가 실행될 때, 다음과 같은 과정이 일어난다.
1. new User("David"); 를 통해 힙 영역에 User 객체가 생성된다.
2. 생성된 User 객체의 name 필드는 David로 초기화된다.
3. 스택 영역의 user 참조변수는 이 힙 영역에 생성된 User 객체를 참조하게 된다.
따라서, user는 스택 영역에 위치하고, new User("David")로 생성된 실제 User 객체는 힙 영역에 위치하며
user 참조변수는 해당 객체의 메모리 주소를 가리킨다.
---------------------------------------------------------------------------------------------------------------------------------------------------------
예를 들어
public class User {
private final String name;
public User(String name) {
this.name = name;
}
}
위의 User 클래스에서 name은 final로 선언되었다. 이 말은 객체가 생성될 때, name 필드에 할당된 값이 그 이후로 변경될 수 없다는 것을 의미한다.
그러나 User 객체를 여러 개 생성할 때마다 각 객체의 name필드에 다른 값을 할당할 수는 있다.
User user1 = new User("Alice");
User user2 = new User("Bob");
user1과 user2는 각각 다른 name을 가지지만, 그 name 값은 변경되지 않는다.
불변성은 객체의 상태가 객체의 생성 후에 변하지 않는 것을 의미한다.
위의 예제에서 User 객체의 name 필드는 객체 생성 후 변하지 않으므로 불변의 상태를 가진다고 할 수 있다.
하지만, 불변성을 갖는 객체와 final 키워드는 약간 다른 의미를 갖는다.
final은 참조의 변경을 막는 것이고,
불변성은 객체의 상태 변경을 막는 것이다.
예를 들어, List객체를 final로 선언한다고 해서
그 List의 내용이 변경되지 않는 것은 아니다. 따라서 완전한 불변성을 원한다면 그 객체의 모든 상태도 불변이어야한다.
(이 부분은 아직 이해를 못했다)
-> 어렵게 계속 final, 불변성 왜 다른 의미인지 생각하지 않고,
생성자 주입 방식이 불변성을 지킬 수 있는 이유는
"생성자는 객체가 생성될 때만 호출되므로, 객체 생성 이후에는 접근할 수 없기 때문이다"
라고 이해해야겠다.
전에도 의존성 주입 방법이 3가지가 있다고 정리했지만
실제 프로젝트에서 사용하려니 하나부터 열까지 궁금한 것 투성이다.
실무를 하려니까 이론을 또 모르겠고, 이론을 하면서 시간을 쏟으니 프로젝트 진도는 안나간다.
내가 이해될 정도로 알아야하는데 이해된다는 건 실제로 실습을 할 줄 알아야하는 것이고, 실습이 안되니 계속 이론을 붙잡을 수 밖에 없다.
그래도 이론을 알아야지 실무를 할 수 있는 건 맞으니 느리더라도 해야겠다.
[Spring] 생성자 주입 vs 필드 주입 (@Autowired)
안녕하세요~ 잭코딩입니다! 이번에는 스프링 프레임워크에서 의존성을 주입하는 방법을 살펴보고 어떤 방식으로 주입하는 게 좋은지 살펴볼까요? 우선 결론부터 말하자면 생성자 주입 (Constructo
jackjeong.tistory.com
https://brownbears.tistory.com/519
[Java] 참조 타입과 참조 변수
기본 타입과 참조 타입자바는 크게 기본 타입(primitive type)과 참조 타입(reference type)으로 분류됩니다. 데이터 타입기본 타입참조 타입정수 타입 배열 타입byte열거 타입char클래스short인터페이스int
brownbears.tistory.com
[Java] 불변객체(Immutable Object)에 대해 알아보자
📚 이번주 멘토링 시간에 책을 꼼꼼히 읽었다고 생각했는데 막상 질문 주신 것에 대해 답변을 못했다. 그 이유는 온전히 내 것으로 만들지 못했기 때문에라고 생각한다. 이번주부터는 읽은 내
yeoonjae.tistory.com
'개인 공부 (23.07~' 카테고리의 다른 글
[프로젝트 문제 해결] 생성자 주입. 기본 생성자가 왜 안되지? 순환참조 발생 (0) | 2023.10.24 |
---|---|
스프링 시큐리티 기본 정리 (1) | 2023.10.24 |
스프링 싱글톤 빈 라이프사이클, @PostConstruct (0) | 2023.10.17 |
스프링 빈 등록, DI, IoC 궁금증 해결 (1) | 2023.10.16 |
인증, 인가, JWT (Json Web Token) (0) | 2023.10.14 |