지난 포스트 에서 프로젝트 구조와 관련된 얘기를 했습니다. 내가 구조화한 프로젝트가 어떤 장점과 단점이 있는지 작성하였고, 프로젝트를 구조화 하는 이유는 도서관에서 책을 카테고리와 특징, 규칙에 따라 분류하는 것과 유사합니다. 그런데 도서관과 프로젝트에 한 가지 차이가 있다면 프로젝트는 다른 책이 또 다른 책의 내용을 빌려오게 되는 것입니다. 그러나 보통 객체지향형 언어에서 A객체가 B객체에 의존할 때 B객체가 변경시, A객체에도 영향이 가는 문제점이 존재합니다.
즉 책 A가 B의 내용을 빌려 의존하고 있는 상황에서 B책을 수정할 때 A책에도 영향이 가는 문제가 발생하는 것입니다. 큰 문제가 없을 수도 있지만, 개발할 때 이는 코드간의 결합도, 종속성이 너무 강력해 유지 보수에 어려움을 겪게됩니다.
이러한 문제를 해결하기 위해 등장한 개념이 의존성 주입(DI, Dependency Injection)입니다. 의존성 주입이란 위 예시에서 처럼 책 A와 B가 존재할 때, 두 책의 의존 관계를 정립하기 위해 외부에 존재하는 IoC라는 컨테이너 내부에서 임시 책(Bean 객체)을 생성해 의존 관계를 정립(주입) 시켜주는 개념입니다.
갑자기 위 얘기들을 장황하게 한 이유는 spring 내에서 api를 설계하면서 mvc패턴을 따르면 너무나 당연하게 다른 객체의 로직을 사용하기 위해 의존성 주입을 하게 됩니다. 그 뿐 아니라 단순한 자바 코드에서도 우리는 생성자를 통해 의존 관계를 정립하게 됩니다.
public class B {
public void doSomething() {
System.out.println("B 객체가 작업을 수행합니다.");
}
}
public class A {
private B b;
// 생성자를 통한 의존성 주입
public A(B b) {
this.b = b;
}
public void useB() {
// B 객체의 메서드를 호출
b.doSomething();
}
}
public class Main {
public static void main(String[] args) {
// B 객체를 생성
B b = new B();
// A 객체를 생성하고 B 객체를 주입
A a = new A(b);
// A 객체가 B 객체를 사용
a.useB();
}
}
이 전까지는 남들이 올린 코드를 따라서 하느라 정확히 객체를 생성할 때나 생성자의 의미를 잘 모르고 코드를 적어 내려갔습니다. 그러나 이번 프로젝트에서는 내가 적는 코드의 의미를 정확히 이해하기 위해 노력하였고, 앞에서 설명한 것처럼 코드의 이미를 잘 이해할 수 있었습니다.
더 나아가 스프링에서는 @Autowired 어노테이션을 통해 의존 관계를 생성자 없이 스프링에서 자동으로 주입해 줄 수 있습니다. 이 전에는 남의 코드를 따라 치기 바빠서 @Autowired의 정확한 의미도 모른 상태에서 코드를 적고, 언제는 생성자를 생성하고 언제는 생성자를 사용하지 않고, @Autowired를 사용할 때는 객체를 생성할 때 final 키워드를 사용하지 않고, 생성자를 통할 때는 객체를 생성할 때 final 키워드를 사용했습니다.
이는 의존성을 주입하는 방식의 차이에서 발생하는 경우들인데 의존성 주입 방식에는 '생성자 주입', 'setter 주입', '필드 주입'이 존재합니다. 아래 본 프로젝트에서 필자가 직접 고민했던 예시를 통해 더 자세히 알아보겠습니다.
- 생성자 주입
@Controller
public class KakaoController {
private final KakaoService kakaoService;
private final UserService userService;
@AutoWired
public KakaoController(KakaoService kakaoService, UserService userService) {
this.kakaoService = kakaoService;
this.userService = userService;
}
}
생성자를 통해 의존성을 주입합니다.
- setter 주입
@Controller
public class KakaoController {
private final KakaoService kakaoService;
private final UserService userService;
@Autowired
public void setKakaoService(KakaoService kakaoService) {
this.kakaoService = kakaoService;
}
@Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}
}
생성자 주입 방식과 유사하지만 setter메소드를 통해서 의존성을 주입받습니다.
- 필드 주입
@Controller
public class KakaoController {
@Autowired
private KakaoService kakaoService;
@Autowired
private UserService userService;
}
정말 간단한 코드로 내 프로젝트의 대부분의 생성자 주입을 이와 같이 했지만, 회고록을 작성한 이유가 또 여기에 있습니다.
필드 주입 방식은 간단하게 코드를 줄여주기 때문에 개발자에게 참을 수 없는 유혹을 합니다. 그러나 편의를 위해서 프로젝트 내 모든 의존 관계를 필드 주입 방식으로 정립하였다면 이는 큰 위험이 초래하게 됩니다. 찾아본 자료를 정리하면 아래와 같습니다.
- SPR(Single Responsibility Principal) 위반
- 이는 객체지향설계 5대 원칙 SOLID의 첫 번째 원칙인 단일 원칙을 위배한 것으로 클래스를 설계할 때 한 가지 책임, 한 가지의 변경 이유만 가져야 하는 뜻을 가집니다.
→ 내가 이해한 결론: 의존성 주입을 코드가 길어지는 만큼 반복해서 한 클래스 내부에서 의존성 주입을 하는 것은 잘못됐다는 의미
- 이는 객체지향설계 5대 원칙 SOLID의 첫 번째 원칙인 단일 원칙을 위배한 것으로 클래스를 설계할 때 한 가지 책임, 한 가지의 변경 이유만 가져야 하는 뜻을 가집니다.
- 의존성 감춤
- DI 컨테이너를 사용한다는 것은 더 이상 클래스들이 스스로 의존성을 관리하지 않는 것을 뜻하고, 이는 의존성을 주입하는데 책임이 DI 컨테이너 혹은 테스트 상황에서는 개발자에게 있다는 것을 의미합니다. 그렇기 때문에 의존성을 주입할 때, public을 통해 생성자 주입 방식과 setter 주입 방식을 사용해서 해당 클래스가 무엇을 필요로 하고, 무엇을 선택사항으로 받는지 명확히 알아야 합니다.
→ 내가 이해한 결론: 오류가 발생했을 때, 원인을 정확히 찾기 위해서
- DI 컨테이너를 사용한다는 것은 더 이상 클래스들이 스스로 의존성을 관리하지 않는 것을 뜻하고, 이는 의존성을 주입하는데 책임이 DI 컨테이너 혹은 테스트 상황에서는 개발자에게 있다는 것을 의미합니다. 그렇기 때문에 의존성을 주입할 때, public을 통해 생성자 주입 방식과 setter 주입 방식을 사용해서 해당 클래스가 무엇을 필요로 하고, 무엇을 선택사항으로 받는지 명확히 알아야 합니다.
- final 키워드 불가
- 필드 주입 방식을 사용하면 생성한 객체에 final 키워드를 적용하지 못합니다. 이 경우 개발자가 실수로 객체를 추가로 생성해 발생하는 오류를 쉽게 잡아내지 못하게됩니다.
- 강한 결합
- 처음 설명에서 보면 알 수 있듯이, 객체간의 의존에는 강한 종속성이 따르기에 의존성 주입을 통해 이 문제를 해결합니다. 특히 spring에서 DI 컨테이너는 의존하는 Bean 객체 간에 의존도를 느슨하게 합니다. 그러나 @Autowired를 통한 필드 주입은 spring을 통해서만 의존성 주입이 가능하기 때문에 강한 결합을 하게 됩니다. 이는 스프링 프레임 워크의 장점을 상쇄하는 모순이 발생하는 것으로 옳바르지 못한 방식입니다.
무조건 기능을 개발하고 서비스를 구축하는 것이 좋기는 하지만 내가 작성한 코드와 설계한 것들이 어떠한 의미를 갖는지 알아가는 과정도 매우 중요하다는 것을 이번 회고를 작성하며 더 깨달았습니다. 내가 객체지향 언어 java를 주 언어로 spring 프레임워크를 통해 개발을 하고 있는데 위 내용들을 모르고 개발을 이어나가는 것은 재앙에 가까운 일이 아닐까 싶은 생각이 들었습니다. 또한 이러한 개념을 더 큰 프로젝트를 경험하기 전에 알아가는 것이 무엇보다 값지다고 생각이 들었습니다.
'개발 > withfriend 🫱🏼🫲🏽🥕' 카테고리의 다른 글
[개발 이슈] 코드에서 민감한 정보는 어떻게 다루지? / application.properties .yml (0) | 2023.10.31 |
---|---|
[개발 이슈] 서비스가 이벤트를 발생시키는 주체가 어떤 사용자인지 어떻게 구분하지? / access token, session, refresh token (0) | 2023.10.30 |
[개발 이슈] 사용자 경험을 고려한 로그인 구현 / OAuth (0) | 2023.10.30 |
[프로젝트 회고] 프로젝트 아키텍처 (0) | 2023.10.24 |
[프로젝트 회고] 파일 구조 (0) | 2023.10.23 |