카테고리 없음

Layered Architecture

k-hamin 2024. 8. 14. 17:42

Spring boot는 layered 아키텍처라는 것을 채택해 코드를 작성한다.

 layered 아키텍처의 각 layer들은 자신이 수행하는 책임을 기준으로 구분되어 있다.

 

쉽게 말해 시스템 구성 요소를 여러 개의 계층으로 분리해 각 계층이 특정 역할을 수행하도록 하는 패턴이다.

 

각 계층사이의 의존성을 줄여 외부 변화로부터 비즈니스 로직의 변화를 막고,

애플리케이션 유지보수와 확장성을 높이려는 목적으로 만들어진 설계의 한 방향이다.


 

크게 Web Layer Service Layer Repository Layer로 나뉜다.


Web Layer(Presentation Layer)

클라이언트의 요청을 알맞은 비즈니스 로직에 전달하는 역할

즉, 서버 애플리케이션 간의 인터페이스를 담당

controller 구현 필요

Service Layer

서버 애플리케이션의 비즈니스 로직을 처리하는 계층

즉, 데이터를 DB에서 가져와 가공하는 계층

service 구현 필요

Repository Layer

서버 애플리케이션이 DB와 상호작용하는 계층

DB에 직접 데이터를 저장, 검색, 업데이트 삭제하는 작업을 처리하며 비즈니스 로직과 데이터 저장소를 이어주는 인터페이스

repository 구현 필요


프로젝트 패키지 구조

domain

비즈니스 로직과 관련된 데이터를 표현하는 객체

이 도메인은 url에서 api를 설계할 때 기준으로 잡았던 리소스와 같다.


각 리소스마다 Web Layer, Service Layer, Repository Layer를 구현해야 한다.

회원 등록, 수정, 삭제, 조회 기능을 구현하려면 각 계층에서 적절한 메서드를 작성하며 된다.

 

회원 등록 코드를 짜보자.

해당 api는 회원 정보(이름, 나이)를 받아 생성된 회원의 id를 return 하도록 한다.

 

코드를 보기 전에, 어노테이션에 대해 알고 가자.


어노테이션이란? (Annotation)

프로그래밍에서 주석은 개발자 입장에서 더 직관적이고 코드를 이해하기 쉽게 하며,

다른 사람에서 설명할 수 있도록 정보를 제공한다.

어노테이션 또한 주석 비슷한 역할을 담당한다고 할 수 있다.

즉, 주석은 사람에서 정보를 제공하는 것이라면, 어노테이션은 프로그램에게 정보를 전달하는 것이라 볼 수 있다.

@를 사용해 작성하며, 해당 타깃에 대한 동작을 수행하는 프로그램 외에는 다른 프로그램에게 영향을 주지 않는다.

 


 

Web Layer( Presentation Layer )

Presentation Layer는 ControllerDispatcher Servlet으로 구성되어 있다.

Dispatcher Servlet는 이미 Spring boot에 구현되어 있고, Controller를 구현해야 한다.

 

 

@RestController //...(1)
public class MemberController {
    
    private final MemberService memberService; //...(2)

    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }  

    @PostMapping("/members")//...(3)
    public ResponseEntity<Long> createMember(  
        @RequestBody MemberCreateRequest request   //...(4)
    ) {
        Long memberId = memberService.createMember(request);  //...(5)
        return ResponseEntity.ok(memberId);  //...(6)
    }
}

 

(1) 해당 클래스가 Restful API를 구현하는 controller라는 것을 명시한다.

 

Restful API

  • 리소스 기준으로 URL를 설계
  • 프런트와 서버가 나누어진 클라이언트-서버 구조
  • HTTP 통신 무상태성을 가짐

(2) .

 

(3) "/members"로 들어오는 POST 요청이 있을 때 아래 메서드가 호출된다.

 

(4) "@RequestBody" 어노테이션은 클라이언트가 요청 본문에 JSON 형식으로 보낸 데이터를 MemberCreateRequest 객체로 변환한다.

 

(5) .

 

(6) ok() 함수를 호출해 HTTP 200 OK 상태 코드를 나타내는 응답을 생성하며 응답 바디에는 memberId가 포함된다.


 

그런데 client가 보낸 request를 해당 controller 코드가 어떻게 알아서 받아줄까?

 

서블릿이 http request를 받아준다.

이 서블릿을 Spring Boot는 Dispatcher Servlet이라는 것으로 구현하고 있다.

 

 Dispatcher Servlet은 특정 url로 요청이 들어오면

해당 요청에 맞는 controller를 스프링 내부에서 찾아

해당 요청과 controller를 연결해 주는 역할을 한다.

그래서 우리는 API url에 맞게 controller만 작성하면 되는 것이다.


 

DTO(data transfer object)

{
"username" : "홍길동",
"age" : "21" 
}

 

request의 body는 위와 같이 JSON 형식으로 이루어져 있는데, java 코드 내에선 JSON 형식을 다룰 수 없다.

 

따라서 JSON을 java로 변환시켜주어야 한다.

 

이것을 직렬화라고 하는데 이는 Spring Boot에서 자동적으로 지원해 준다.

우리는 해당 JSON에 맞는 java record 클래스를 만들어주면 된다.

 

dto 패키지에 MemberCreateRequest 클래스를 만들어준다.

public record MemberCreateRequest(
    String username,
    int age
) {
}

record 타입은 Java 17에서 DTO를 위해 만들어진 데이터 타입이다.

 

Service Layer

 

spring boot는 관습적으로 MemberService 인터페이스를 정의해 Service에서 구현할 메서드를 구현하고 MemberServiceIml 클래스를 구현하는 경우가 많다.

 

public interface MemberService {
    Long createMember(MemberCreateRequest request);
}

@Service
public class MemberServiceImpl implements MemberService{

    private final MemberRepository memberRepository;

    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    @Override
    public Long createMember(MemberCreateRequest request) {

        // 멤버 객체 생성
        Member member = new Member(request.username(), request.age());
        
        // 멤버 DB에 저장
        Member save = memberRepository.save(member);

        return save.getId();
    }
}

 

여기서 member클래스를 entity라고 한다.

entity를 repository를 통해 DB에 저장하는 것이다.


 

MemberController의 memberService의 타입은 인터페이스였는데

memberService.createMember(request);

어떻게 이렇게 실제 메서드처럼 쓰는 것처럼 쓰이는 것일까?

 

Spring Boot가 자동으로 해당 인터페이스를 구현하고 있는 MemberServiceImpl 클래스를

생성자의 파라미터에 주입해 주기 때문이다.


Repository Layer

 

Repository Layer는 Entity Repository Interface로 구성되어 있다.

 

 

Entity

 

DB 테이블과 매핑되는 자바 클래스로 아래와 같이 구성된다.

@Entity 
public class Member {

    @Id //DB에서 ID 역할을 함
    @GeneratedValue(strategy = GenerationType.IDENTITY) //DB에 entity를 추가하면 ID가 자동으로 1,2,3 순서로 증가되며 할당됨
    @Column(name = "user_id") //DB에 user_id라는 이름으로 변경되어 저장됨
    private Long Id;

    private String name;

    private int age;
    
    public Member() {
    }

    public Member(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public Long getId() {
        return Id;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

 

Repository Interface

DB에 entity를 저장, 가져오기, 수정하기, 삭제하기 등을 수행하는 repository는 JPA라는 기술을 사용한다.

 

이를 위해 JPA 라이브러리 의존성을 추가해 준다. 

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

//JPA
implementation 'org.springframework.boot:spring-boot-starter-data-jpa' //추가
}

 

repository 코드는 아래와 같다.

public interface MemberRepository extends JpaRepository<Member, Long> {
}

 

위와 같이 생성하면 아무것도 없는 것 같지만 자동으로 CRUD 메서드를 제공한다.

  • memberRepository.save(entity)
  • memberRepository.update(entity)
  • memberRepository.findById(id)
  • memberRepository.delete(entity)

등등..

 

근데 인터페이스인데 어떻게 돌아가지?

이는 JPA라는 기술이 JpaRepository <Memeber, Long>만 상속하면 알아서 구현 클래스를 생성해 주기 때문이다.

@Service
public class MemberServiceImpl implements MemberService{

    @Autowired
    private MemberRepository memberRepository; // 추후 설명한다는 곳

    @Override
    public Long createMember(MemberCreateRequest request) {

        // 멤버 객체 생성
        Member member = new Member(request.username(), request.age());
        
        // 멤버 DB에 저장
        Member save = memberRepository.save(member);

        return save.getId();
    }
}

 

    @Autowired
    private MemberRepository memberRepository;

위와 같이 @Autowired를 달아두면 Spring Boot가 알아서 JPA가 자동으로 만든 해당 구현 클래스를 주입해 준다.

 

DataBase

 

entity 데이터를 저장할 데이터베이스를 구축해주어야 한다.

 

실제 프로젝트에서는 MySQL, Postgresql 등의 데이터베이스를 사용해야 하지만

학습용으로 인메모리 DB인 H2를 사용하자

 

인메모리라는 것은 Database가 Spring Boot 안에 생성되며 서버 종료 시 DB 내 데이터가 모두 소실되는 DB이다.

 

build.gradle에 아래 코드를 추가해 준다.

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

//JPA
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

//DB
implementation 'com.h2database:h2'
}

 

이후 프로젝트에서 resources 디렉터리 안의 application.properties를 지우고,

application.yml로 바꿔준다.

 

spring:
  datasource:
    url: jdbc:h2:mem:testdb
    driver-class-name: org.h2.Driver
    username: sa
    password:
  h2:
    console:
      enabled: true
      path: /h2-console
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
    properties:
      hibernate:
        format_sql: true
        use_sql_comments: true

위와 같이 써준 후, 서버를 실행시킨다.

 

http://localhost:8080/h2-console로 접속!

URL을 yml에서 설정한 url로 바꾸고 connect 클릭

위와 같이 뜨면 성공이다.


 

이번엔 API 요청을 직접 쏴보자

 

 

이를 위해 API 문서 생성 및 테스트를 도와주는 swagger라는 라이브러리를 사용한다.

build.gradle에 swagger 의존성을 추가해 준다.

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

	//JPA
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

	//DB
	implementation 'com.h2database:h2'

	//swagger
	implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2'
}

아까 만들었던 controller가 보이게 되며
Try it out 클릭
데이터 원하는데로 수정하고 Execute 누르면 요청이 백엔드 서버로 전송된다.

 

응답이 200번으로 오며
실제 DB에도 해당 데이터가 생성된 것을 볼 수 있다.