스프링 프로젝트를 시작하기 앞서
프로젝트를 구조화하기 위해서 아키텍처를 정해야 했다.
내가 선택했던 아키텍처는 '계층형 아키텍처(Layered Architecture)'이다.
계층형 아키텍처를 적용하기로 했던 프로젝트는
DB로 MySQL을 사용하고, Mybatis를 추가로 사용했다.
이 점이 계층형 아키텍처랑 잘 어울리는 부분이라고 생각했다.
우선 간단한 계층형 아키텍처의 모습을 보자
✅ 계층형 아키텍처(Layered Architecture)
클라이언트의 HTTP 요청이 오면
1차적으로 컨트롤러가 응답한다.
이후 'Service Layer'와 'Model'로 요청이 흐르는데
여기서 Service Layer는 스프링에서 서비스 클래스와 같고,
Model은 데이터 접근 객체(dto, entity)와 같다.
해당 아키텍처에서 컨트롤러는 DB와 직접적인 연결을 하지 않는다.
이를 스프링 MVC 흐름과 함께 자세히 정리해보자
✅ 스프링 MVC와 계층형 아키텍처
스프링 MVC 흐름을 짧게 정리하면 다음과 같다.
1. 핸들러 매핑을 통해 사용자 요청에 매핑된 핸들러(Controller)를 조회
2. 핸들러를 실행할 수 있는 핸들러 어댑터를 조회
3. 핸들러 어댑터 실행
4. 핸들러 어댑터가 핸들러를 실행
5. 핸들러가 'ModelAndView'를 반환
6. 뷰 리졸버(View Resolver) 뷰 객체를 반환
7. 뷰 랜더링
위의 흐름은 계층형 아키텍처에서 '표현 계층(Presentation Layer)'에 속한다.
만약 프로그램이 너무 간단하여 사용자 요청 시 화면에 'Hello world'만 출력하면 된다면
해당 프로그램은 표현 계층으로만 완벽히 아키텍처를 설명할 수 있다.
하지만 실제 프로그램은 더욱 복잡하다.
요청이 오면 DB에 접근하여 요청에 따른 데이터를 반환하거나 저장, 삭제해야 하고
단순 'Hello world' 출력이 아니라, 어떤 요청일 때는 어떤 식으로 데이터를 처리하여
화면에 출력한다는 식의 복잡한 로직이 구현돼야 한다.
때문에 표현 계층 외에도 그림에서와 같이
'비즈니스 계층(Business Layer)'과 '데이터 접근 계층(Data Access Layer)'이 필요하다.
✅ 비즈니스 계층과 데이터 접근 계층이 나뉘는 이유
간단한 예를 들어 보자
현재 로컬 환경에서 개발 중이고 요청 포트는 8080번 포트이다.
사용자가 다음과 같은 요청을 만들었다.
http://localhost:8080/10
이런 요청에 대해서 처리해야 하는 작업은 다음과 같다.
1. '/'뒤에 오는 숫자에 대해 10을 더해야 한다.
2. 10을 더한 숫자가 DB에 있는지 조회해야 한다.
3. 있으면 DB에 저장된 숫자에 맞는 회원 아이디를 조회한다.
4. 조회한 결과를 화면에 출력한다.
❗ 현재 DB 구성 예시
number | id |
10 | Hello |
15 | world |
20 | Determination |
아까 단순 화면에 'Hello world'를 요청하는 것보다 훨씬 복잡해졌다.
그럼 순서대로 어떤 흐름을 거치는지 알아보자.
우선 숫자 요청이 들어오면 반응하는 컨트롤러가 있다고 가정하자.
우선적으로 해당 컨트롤러가 호출된다.
해당 컨트롤러에서는 요청에 대해 숫자 '10'을 더하는 로직을 실행해야 한다.
이때 해당 로직을 구현하기 위해 Service 클래스를 만든다.
Service 클래스는 핵심 비즈니스 로직을 구현하고 DB와 접근하는 객체이다.
이 Service 클래스는 비즈니스 계층에 속한다.
서비스 계층에서는 다음 작업이 일어난다.
1. 10에 10을 더해 20을 만든다.
2. DB에 20이란 숫자를 가지고 접근한다.
여기서 서비스 계층은 DB에 직접적으로 접근하지 않는다.
보통 'DAO'라고 불리는 클래스를 만들어 DB와 관련된 로직을 따로 분리해준다.
DAO 클래스는 데이터 접근 계층에 속한다.
DAO 클래스는 MySQL이나 오라클과 같은 DB에 접근하여
데이터의 조회나, 수정, 삭제 등을 담당한다.
현재 요청은 20에 해당하는 아이디를 조회하는 것이기 때문에
DAO 클래스는 '20'을 통해 'Determination'이라는 ID를 조회하여 반환한다.
반환된 ID는 요청 흐름을 역으로 타고 올라가
서비스 클래스에서 다시 컨트롤러로 전달되고,
전달된 객체는 ModelAndView로 반환되어 화면에 랜더링 되게 된다.
이것이 전반적인 계층형 아키텍처와 스프링 MVC의 흐름이다.
✅ DTO와 Entity
여기서 하나 빠진 것이 바로 DTO 객체이다.
DTO 객체는 쉽게 말해 '데이터 전달 객체'이다.
10, 20, 그리고 'Determination'이라는 데이터는
DTO 객체에 의해 전달되어진다.
이때 중요한 것이 DTO 객체를 DB에 직접적으로 접근시키지 않는 것이 좋다는 것이다.
위의 흐름에서 10이라는 데이터는 20으로 전환되었다.
이 값이 DTO를 통해 전달된다고 할 때 데이터를 변경하는 방법은 'setter'를 이용하거나 생성자를 이용하는 것이다.
예를 들어보자
// 1. setter
numberDto.setNumber(20);
// 2. 생성자
NumberDto number = new NumberDto(20);
각각 setter와 생성자가 구현되어 있다고 할 때
위의 방식으로 데이터를 변경할 수 있다.
여기서 'setter'를 사용하는 것이 DTO 객체를 DB에 직접적으로 접근시키지 말아야 할 이유가 된다.
DTO는 각 계층 간 데이터 전달을 담당하는 객체이다.
따라서 데이터가 언제든지 변경될 수 있다.
이렇게 변경될 수 있는 객체를 DB에 직접적으로 접근시키면 문제가 된다.
예를 들어 내가 전달해야 하는 값이 '10+10'이어서 '20'이라면
이때부터 20이라는 값이 담긴 객체는 '불변 객체'여야 한다.
비즈니스 로직 결과로 나온 값이 20인데 임의로 30으로 바꿔서
DB 조회를 하면 의미가 없기 때문이다.
하지만 setter 사용이 가능한 객체라면 가변 객체가 되기 때문에
DB 접근 객체로 DTO 클래스를 사용하는 것은 좋지 않다.
이러한 문제를 해결하기 위해 기본 생성자가 포함되어있고, setter 사용이 불가능한
엔티티(Entity) 객체를 따로 만든다.
이 엔티티 객체를 통해 DB에 접근하고, 엔티티 객체를 통해 DB 조회 결과를 전달받는다.
✅ 실제 프로젝트 사용 예시
실제 나의 프로젝트를 통해 다시 한번 확인해보자.
http://localhost:8080/join
위의 요청을 받을 컨트롤러는 다음과 같다.
@PostMapping
public String join(@ModelAttribute("memberDto") MemberDto memberDto) throws Exception {
//회원가입
memberService.join(memberDto);
return "redirect:login";
}
'MemberDto'객체에 요청 정보를 담아서 MemberService 객체로 전달한다.
public MemberDto join(MemberDto memberDto) throws Exception {
//dto to entity
Member member = memberDto.toEntity(memberDto);
//회원 정보 저장
Integer result = memberMapper.insert(member);
if(result != null) {
return new MemberDto().toDto(member);
} else {
return null;
'MemberService' 객체에서는 우선 전달받은 DTO 클래스를 Entity 클래스로 전환한다.
전환된 엔티티는 DB에 접근하는 객체로 활용된다.
(위에서는 DAO 클래스를 사용한다고 했는데 나는 Mybatis의 Mapper 인터페이스를 대신 활용했다.)
저장이 잘 되었다면 다시 엔티티 객체를 DTO 객체로 전환하여 컨트롤러로 전달한다.
앞서 말했듯이 계층 간 데이터 이동은 DTO 객체가 담당하기 때문에
엔티티 객체를 DTO 객체로 전환해준다.
✅ 추가 내용
- Dto 클래스에서도 setter 사용을 금하기도 한다.
- 이럴 때는 보통 생성자나 Builder 패턴을 사용한다.
- 이러한 아키텍처를 사용하는 이유는 코드의 응집도를 낮추고 유지보수를 쉽게 하기 위해서이다.
'내가 공부하려고 올리는 > 스프링' 카테고리의 다른 글
프로젝트를 더욱 S.O.L.I.D 하게 수정해보기 (0) | 2022.07.12 |
---|---|
나는 ServiceImpl을 잘 활용했는가? (0) | 2022.07.11 |
빈 스코프 (0) | 2022.02.06 |
빈 생명 주기 콜백 (0) | 2022.02.04 |
의존 관계 주입 (0) | 2022.02.03 |
댓글