3 Layer 변경 직후 문제점
메모 실습 소스코드는 3 layer로 Controller-Service-Repository로 역할과 기능에 따라 분리 됬지만, 각 계층마다 jdbcTemplate를 사용하는 객체를 각 메소드마다 생성하는 중복 문제가 있다. 예를 들어 MemoService.java의 모습은 다음과 같다.
public class MemoService {
private final JdbcTemplate jdbcTemplate;
public MemoService(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public MemoResponseDto createMemo(MemoRequestDto requestDto) {
// DB 저장
MemoRepository memoRepository = new MemoRepository(jdbcTemplate);
...
}
public List<MemoResponseDto> getMemos() {
// DB 조회
MemoRepository memoRepository = new MemoRepository(jdbcTemplate);
...
}
public Long updateMemo(Long id, MemoRequestDto requestDto) {
MemoRepository memoRepository = new MemoRepository(jdbcTemplate);
...
}
public Long deleteMemo(Long id) {
MemoRepository memoRepository = new MemoRepository(jdbcTemplate);
...
}
}
Java
복사
MemoRepository memoRepository = new MemoRepository(jdbcTemplate); 이 부분이 각 CRUD 메소드마다 중복되고 있다.
결상국 JdbcTemplate가 필요한 부분들이 중복되기 때문에 각 레이어마다 Controller는 Service클래스 객체를, Service는 Repository클래스 객체를, Repository는 JdbcTemplate클래스 객체를 생성하도록 연결하면 3개 레이어가 모두 JdbcTemplate의 멤버(기능)을 가진 객체를 본인 클래스에서 사용하게 된다.
1.
JDBC가 직접적으로 필요한 Repository.java에서 JdbcTemplate 클래스의 멤버(기능)을 갖는 객체 Repository타입의 생성자를 통해 객체 생성
2.
Repository가 필요한 Service.java에서는 Repository 클래스의 멤버(기능)을 갖는 객체 Service타입의 생성자를 통해 객체 생성
3.
Service가 필요한 Controller.java에서는 Service 클래스의 멤버(기능)을 갖는 객체 Controller타입의 생성자를 통해 객체 생성
하면서 3개의 레이어가 모두 결과적으로 JDBC를 멤버로 가지게 되며 JdbcTemplate-Repository-Service-Controller 까지 필요 멤버가 포함된 객체를 생성(New)하면서 코드의 중복을 줄일 순 있다.
그 중 Service.java의 예시는 다음과 같다.
public class MemoService {
// 멤버 변수 선언
private final MemoRepository memoRepository;
// 생성자: MemoService(JdbcTemplate jdbcTemplate)가 생성될 때 호출됨
public MemoService(JdbcTemplate jdbcTemplate) {
// 멤버 변수 생성
this.memoRepository = new MemoRepository(jdbcTemplate);
}
public MemoResponseDto createMemo(MemoRequestDto requestDto) {
// RequestDto -> Entity
Memo memo = new Memo(requestDto);
// DB 저장
Memo saveMemo = memoRepository.save(requestDto);
// Entity -> ResponseDto
MemoResponseDto memoResponseDto = new MemoResponseDto(saveMemo);
return memoResponseDto;
}
...
}
Java
복사
위 처럼 기본적으로 memoRepository 객체가 동일 클래스에 있는 각 메소드마다 MemoRepository memoRepository = new MemoRepository(jdbcTemplate); jdbc를 가진 객체 생성을 대신하기 때문에 클래스 내부의 메소드의 코드 중복은 해결이 된다. 하지만,
문제는 강한 결합
클래스의 포함 관계에 따라 이처럼 구성하면
Controller도 결국 jdbc를 가지고 있는 Service 객체를 요구하는 상황,
또 이어서 Service도 Repository클래스의 객체를 생성하는 요구 상황,
Repository는 JDBC의 객체를 요구하는 상황이며
기능을 위해서 하위 계층 타입의 객체를 생성하는 모습이다.
혹시나 Repository에서 다른 매개변수가 추가된다면 Service도, Controller도 연결되어 해당 인자를 하위 레이어에 전달해서 객체 생성 시 매개변수로 받아와야 하는 문제점이 생긴다.
이렇게 연결된 클래스 간의 변동사항이 생기는것에 취약해지게 되는 것이 강한 결합의 단점이다. 또한 흐름이 Controller→Service→Repository→JdbcTemplate 클래스 순서로 흘러가고 있다.
강한 결합을 풀 수 있는 방법은?
아래처럼 객체 생성을 한번만 진행하고 다른곳에서 재사용하는 과정을 거치면 된다.
1. 각 객체에 대한 객체 생성은 딱 1번만!
2. 생성된 객체를 모든 곳에서 재사용!
3. 생성자 주입을 사용하여 필요로하는 객체에 해당 객체 주입!
Repository부터 객체를 생성한다 하면 윗 계층으로 객체 자체가 전달되는 과정은 다음과 같다.
Repository.java
public class MemoRepository {
private final JdbcTemplate jdbcTemplate;
//jdbc가진 생성자
public MemoRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
Java
복사
Service.java
public class MemoService {
private final MemoRepository memoRepository;
public MemoService(MemoRepository memoRepository) {
//this.memoRepository = new MemoRepository(jdbcTemplate);
//위는 이전 코드 강한 결합을 아래 하위계층 객체를 그대로 사용한다.
this.memoRepository = memoRepository;
}
Java
복사
Controller.java
public class MemoController {
private final MemoService memoService;
public MemoController(MemoService memoService){
//this.memoService = new MemoService(jdbcTemplate);
//위는 이전 코드 강한 결합을 아래 하위계층 객체를 그대로 사용한다.
this.memoService = memoService;
}
Java
복사
이처럼 하위 계층의 객체를 그대로 사용하면서 느슨한 결합으로 풀어 낼 수 있다. 이처럼 흐름 또한 Repository→Service→Controller 순서로 객체가 활용되고 있다. 이런 모습을 제어의 역전 IoC라고 할 수 있다. 요약하면 구현체 내부가 아닌, 외부에서 프로그램 제어를 하는 것이 제어의 역전(IoC)이다.
IoC의 대표적인 사례가 바로 프레임워크 자체이다. 우리가 프레임워크를 이용해 프로그램을 구현하면, 프레임워크 위에서 프로그램이 실행된다.(main 메소드는 @SpringBoot를 통해 프레임워크위에서 동작하게 된다).
이 말은 구현한 프로그램의 설정과 실행 등의 제어를 프레임워크가 해준다는 의미이며 제어권을 프로그램이 가지고 있는 게 아니고, 프레임워크 가지고 있는 형태이다. 이것이 곧 제어가 역전된 상태인 것입니다. 그래서 IoC의 대표적인 예는 프레임워크가 되는 것이다.
하지만 오류가 발생한다 아직. 객체는 어디서 누가 주입해준것인가? IoC Container의 역할?
하지만 객체 생성부분이 없어서 오류가 발생. 객체는 어디서 생성해야하지? New가 안보인다. 뭐 상속도 인터페이스도 아니고 extends던 implements던 뭐가 있어야 되는것 아닌가?
동적인(호출시점에 생성되거나 아닐수도있거나 하는) 객체 인스턴스들은 실행 시 어떤 구현객체가 선택 될지 결정되는 의존관계를 가지고 있다. 쉽게 객체가 생성 되면(동적) 그때서야 그 멤버들을 사용할 수 있다는 것이 증거이다. 의존관계 주입은 바로 이 동적인 의존관계를 외부에서 주입해 주는 것이다.
의존관계 주입(DI)을 사용하여 동적인 인스턴스를 주입하면(호출하면), 클라이언트의 변경 없이 호출 인스턴스를 변경할 수 있다는 장점이 있다. 즉, 정적인 클래스 다이어그램, 클래스 의존관계의 변경 없이 동적 인스턴스를 변경할 수 있게 된다.
기본적으로 Java에서 배운 클래스는 객체 인스턴스 생성(메모리에 올림)을 해야 그 인스턴스를 통해 도트 연산자로 메소드던 필드던 멤버를 사용 할 수 있었다. 그럼 애초에 jdbc는 어떻게 불러졌는지 생각해보면 의문이다. Repository는 JdbcTemplate 클래스를 상속받지도 않았는데 사용하고 있었다.
Spring에서 IoC Container(Bean을 모아둔 컨테이너) 또는DI 컨테이너로 불리우는 것은 클래스를 등록하면 Bean 클래스로 등록되며 사용 할 수 있다. Spring Framework의 IoC 제어의 역전을 보여 주는 대표 동작원리는 바로 IoC Container에 등록하는 의존성 주입(DI) 행위라고 할 수 있다.
실제 DI (class→IoC Container)구현 방법 = @Component 어노테이션
개념적인것은 어느정도 이해가 가고있다. 실제 프로젝트에선 어떤 방식으로 그 DI를 하는가?
기본적으로 지금 필요한 주입 대상인 Controller, Service, Repository는 @Component라는 어노테이션을 통해 IoC Container에 Bean으로 등록 할 수 있다. 물론 다른 클래스도 주입 가능하다.
하지만 Controller, Service, Repository는 구조에서 핵심적인 계층이기 때문에 Spring에서도 @Controller 또는 @RestController,@Service,@Repository 처럼 별도의 명시적인 어노테이션으로 구분해주고 있는데, 해당 어노테이션을 따라 실제 인터페이스 구현체를 살펴보면 해당 인터페이스 자체가 @Component 가 적용되어 있는 것을 볼 수 있다. 이에 따라서 동일하게 IoC Container안에 Bean으로 등록 되는 것이다.
Controller Interface의 구현체 모습(@Controller 어노테이션 따라들어가기)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {
/**
* The value may indicate a suggestion for a logical component name,
* to be turned into a Spring bean in case of an autodetected component.
* @return the suggested component name, if any (or empty String otherwise)
*/
@AliasFor(annotation = Component.class)
String value() default "";
}
Java
복사
이와 같이 @Component어노테이션을 클래스 위에 작성하게 되면 대상 클래스는 Bean에 등록되어 주입되는 대상이 되는 것이며 모두 Spring 내부에서 객체 생성 없이 사용 할 수 있게 되는 것이다.
@Controller// Controller 내부의 @Component 에 따라 bean으로 등록 됨
public class MemoController {
Java
복사
@Service// Service 내부의 @Component 에 따라 bean으로 등록 됨
public class MemoService {
Java
복사
@Repository// Repository 내부의 @Component 에 따라 bean으로 등록 됨
public class MemoRepository {
Java
복사
ps. 멘토님과의 설명으로 내용이 추가되었다.
DI와 IoC Container, IoC 제어의 역전에 대한 것도 정리한 것을 천천히 정리하면서 말해보는 연습을 진행했다. 생각보다 처음엔 어디부터 설명을 해야 할지, 정리는 했지만 그 관계를 내가 정확하게 파악하고 있는지 테스트 할 수 있는 좋은 기회였다. 처음엔 생각보다 시작이 어려웠지만, 점점 말을 하면서 그 안에서 또 정리된 내용이 한번 더 정리되었다. 강한 결합의 단점과 느슨한 결합으로 만드는 방법, 그리고 그 느슨한 결합을 구현하는데 필요한 사항과 그 부분에서 Spring의 DI, IoC Container의 역할은 무었이며, 실제로 어떤 식으로 구현되는지에 대한 설명으로 정리가 되면서 나 스스로도 이해도가 한번 더 단단해졌다. 그리고 @Component와 @Controller, @Service, @Repository 는 어떻게 다른지에 대해서 부정확하게 알고 있던 부분에 대해서 피드백을 받았으며, 그것 또한 DI와 연결되어 있는 것을 확인하면서 여기서도 기본기의 중요성을 또 한번 깨닫게 되었다.