[SoloProject] To-Do App

YuLim·2023년 2월 8일
2

💻 To-Do List를 구현하는 기본 CRUD 구현 솔로 프로젝트


🔎 API 계층

✔ Controller (ListController.java)

Handler Method(핸들러 메서드)

  • 클라이언트의 요청을 처리하는 Controller의 메서드
  • 클라이언트의 요청 데이터를 Service 클래스로 전달하고, 응답 데이터를 클라이언트로 다시 전송하는 역할

DI(Dependency Injection)

생성자 DI(생성자 의존성 주입)
Spring의 DI 기능을 이용해서 Controller생성자 파라미터로 Service의 객체를 주입 받는다.
Spring이 애플리케이션 로드 시, ApplicationContext(스프링 컨테이너)에 있는 Service객체를 주입해준다.

Spring의 DI는 주입을 받는 클래스와 주입 대상 클래스 모두 Spring Bean이어야 한다.
Controller : @RestControllerSpring Bean이다.
Service : @ServiceSpring Bean이다.

※ 생성자가 하나일 경우 Spring이 자동으로 DI를 적용한다.
하지만, 생성자가 둘 이상일 경우 DI를 적용하기 위한 생성자에 반드시 @Autowired를 붙여야 한다.

CRUD

  • Create(Post)
    • PostList() : 할 일 목록 등록
  • Read(Get)
    • getList() : 특정 id 목록 조회
    • getLists() : 전체 목록 조회
  • Update(Patch)
    • patchCheck() : 할 일 완료 표시
    • patchList() : 내용 수정
  • Delete(Delete)
    • deleteList() : 특정 id 목록 삭제
    • deleteLists() : 전체 목록 삭제

DTO

Data Transfer Object
데이터를 전송하기 위한 용도의 객체
요청 데이터를 하나의 객체로 전달 받는 역할(클라이언트 측으로부터 전달 받은 request body에 매핑되는 DTO 클래스)
즉, @RequestParam을 이용해 각각의 파라미터를 전달 받는 대신에 DTO클래스를 이용해 한번에 전달

  • getter 사용 이유 : Controller에서 response dto를 response body로 전달
    • MappingJackson2HttpMessageConverter가 response dto를 JSON 문자열로 변환할 때 getter 사용
  • setter 사용 이유 : 핸들러 메서드에서 path variable로 전달 받은 필드를 setter를 통해 채움

@RequestBody

JSON 형식의 Request Body를 DTO클래스의 객체로 변환 시켜주는 역할(JSON 역직렬화, Deserialization)

ResponseEntity

ex) return new ResponseEntity<>(dto, HttpStatus.CREATED); 처럼 ResponseEntity 객체를 생성하면서 생성자 파라미터로 응답 데이터(dto)와 HTTP 응답 상태를 함께 전달할 수 있다.
클라이언트 측에 전송하는 response body용 정보(DTO, HTTP 응답 상태)가 포함된 ResponseEntity

핸들러 메서드에 @ResponseBody가 붙거나 핸들러 메서드의 리턴 값이 ResponseEntity일 경우, 내부적으로 HttpMessageConverter가 동작하게 되어 응답 객체(DTO 클래스의 객체)를 JSON 형식으로 바꿔준다.(JSON 직렬화, Serialization)

  • 메서드를 이용하는 방법
    • ResponseEntity.created(location).build();
  • 생성자를 이용하는 방법
    • new ResponseEntity<>(dto, HttpStatus.CREATED);

ResponseEntity와 location(URI)

일반적으로 클라이언트 측에서 백엔드 애플리케이션 측에 리소스의 등록(POST)을 요청할 경우, 백엔드 애플리케이션은 해당 리소스를 DB에 저장한 후 요청한 리소스가 성공적으로 저장되었음을 알리는 201 Created Http Status를 reponse header에 추가해서 클라이언트 측에 응답으로 전달한다.
그리고 추가적으로 DB에 저장된 리소스의 위치를 알려주는 위치 정보 URI도 response header에 추가해서 응답으로 전달한다.
클라이언트 측에서는 response header에 포함된 리소스의 위치정보 URI를 얻은 후에 해당 리소스의 URI로 다시 요청을 전송해서 리소스의 정보를 얻어온다.

UriComponentsBuilder
URI를 직접 작성하는 것보다 편리하고 정확하게 URI를 생성한다.
UriComponents(URI를 구성하는 Components들을 다룰 수 있도록 하는 클래스, 생성자가 private이기 때문에 직접 구현 불가)를 Build할 수 있도록 도와주는 클래스이다.
UriComponentsBuilder newInstance() : UriComponentsBuilder 객체 생성
UriComponentsBuilder path(String) : URI 구성요소 설정
UriComponents buildAndExpand(Object... uriVariableValues) : URI템플릿 변수를 설정한 후, UriComponents 인스턴스 Build
URI toUri() : UriComponents인트턴스를 URI로 변환


※ URI { } 템플릿 변수를 지정하면 buildAndExpand()에 순서대로 실제 값 지정 필요
※ UriComponentsBuilder의 메서드들은 반환타입이 UriComponentsBuilder이므로 메서드 체이닝으로 호출 가능하다.


🔎 Mapper

DTO클래스와 Entity클래스를 서로 변환해주는 클래스
@Component로 Spring Bean에 등록 필요

MapStruct

매퍼 클래스를 자동으로 구현해준다.
객체들 간의 변환 기능을 제공하는 Mapper 인터페이스

@Mapper(componentModel = "spring")
public interface ListMapper {
	...
}

@Mapper : 해당 인터페이스는 MapStruct의 매퍼 인터페이스로 정의
@Mapper(componentModel = "spring") : componentModel = "spring" 애트리뷰트 지정 시 Spring Bean으로 등록

  • MapStruct 인터페이스의 구현 클래스(MapperImpl.java) 생성
    • MapStruct가 Mapper인터페이스를 기반으로 Mapper구현 클래스를 자동으로 생성해준다.
    • [Gradle] 탭의 [프로젝트 명 > Tasks 디렉토리 > build 디렉토리 > build task]를 실행하면 자동으로 생성됨
    • [Project 탭 > 프로젝트 명 > build] 디렉토리 내 Mapper인터페이스가 위치한 패키지 안에 클래스 생성
  • MapStruct 인터페이스의 구현 클래스인 MapperImpl 클래스에서 사용되므로 DTO와 Entity에 각각 필요한 롬복 애너테이션 활용
    • DTO(Post, Patch) 필드.get(@Getter) > 변환 > @NoArgsConstructor + Entity 필드.set(@Setter)
    • Entity 필드.get(@Getter) > 변환 > @AllArgsConstructor DTO(Response)
      • ResponseDto는 JSON직렬화를 위해서 @Getter도 필요
    • 각 DTO와 Entity에 @AllArgsConstructor 테스팅에서 필요하다는데 확인해보고 추가하기

복잡한 DTO클래스와 Entity클래스의 매핑은 MapStruct에 default메서드를 직접 구현해서 개발자가 직접 매핑 로직을 작성할 수 있다.


🔎 Service 계층 (Business Logic)

✔ Service (ListService.java)

API 계층과 Service 계층을 연동한다. = API 계층에서 구현한 Controller 클래스가 Service 계층의 Service 클래스와 메서드 호출을 통해 상호 작용한다.

Controller 클래스의 핸들러 메서드와 1대1로 매치 된다.

Spring의 DI 기능을 이용해서 Service생성자 파라미터로 Repository의 객체를 주입 받는다.

Entity

Service 계층에서 Data Access 계층과 연동하면서 비즈니스 로직을 처리하기 위해 필요한 데이터를 담는 역할을 하는 클래스

API 계층에서 사용한 모든 DTO클래스의 멤버 변수들이 포함되어 있다.


🔎 Data Access 계층

✔ JDBC

public interface ListRepository extends CrudRepository<ToDoList, Integer> {

}

Data Access 계층에서 DB와 연동을 담당하는 Repository 인터페이스이다.

CrudRepository<ToDoList, Integer>에서 ToDoList는 ToDoList 엔티티 클래스를 가리키며, Integer은 ToDoList 엔티티 클래스에서 식별자를 의미하는 @Id애너테이션이 붙은 멤버 변수의 타입을 가리킨다.
(@Id애너테이션이 붙은 필드는 DB 테이블의 PK컬럼과 매핑된다)
이와 같이 제너릭 타입을 지정함으로써 해당 엔티티 객체의 데이터를 DB 테이블에 생성, 조회하거나 DB에서 조회한 데이터를 해당 엔티티 클래스로 변환할 수 있다.

CrudRepository는 DB에 CRUD(데이터 생성, 조회, 수정, 삭제) 작업을 위해 Spring에서 지원해주는 인터페이스이다.
CRUD에 대한 기본적인 메서드가 정의되어 있다.

listRepository인터페이스 구현 클래스 객체는 Spring Data JDBC에서 내부적으로 Java의 리플렉션 기술과 Proxy기술을 이용해서 생성해준다.

Entity

ToDoList 엔티티 클래스 명은 DB의 테이블 명에 해당한다.

@Id애너테이션을 추가한 필드는 해당 엔티티의 고유 식별자 역할을 하고, 이 식별자는 DB의 Primary key로 지정한 컬럼에 해당한다.

schema.sql

CREATE TABLE IF NOT EXISTS TODOLIST (
    id bigint NOT NULL AUTO_INCREMENT,
    ...
    PRIMARY KEY (id)
);

id는 TODOLIST테이블의 PK이고 AUTO_INCREMENT를 지정했으므로 데이터가 insert될 때마다 자동으로 증가된다.
즉, 애플리케이션에서 DB에 데이터를 insert할 때 id컬럼에 값을 지정해주지 않아야 한다.

ToDoList(엔티티)클래스 명은 DB의 TODOLIST테이블 명과 매핑되고 엔티티 클래스의 각각의 필드들은 DB 테이블 컬럼에 1:1 매핑된다.
대소문자 구분없이(SQL 대소문자 구분 'X') ToDoList엔티티 명과 DB 테이블 TODOLIST 명과 이름이 같으면 매핑된다.

@Table애너테이션을 엔티티에 추가하지 않으면 기본적으로 엔티티 클래스 명이 테이블의 이름과 매핑된다.
@Table("TODO")와 같이 매핑되는 테이블 이름을 TODO로 변경할 수 있다.

✔ JPA

JPA(Java Persistence API, Jakarta Persistence) : Java진영에서 사용하는 ORM(Object Relational Mapping)기술의 표준 사양(또는 명세)이다.
표준 사양(또는 명세)이라는 의미는 Java의 인터페이스로 사양이 정의되어 있어 구현체는 따로 있다는 것을 의미한다.
즉, JPA의 구현체에 대해서 공부하면 된다고 합니다.

Hibernate ORM

JPA 표준 사양을 구현한 구현체
JPA에서 지원하는 기능 외에 Hibernate자체적으로 사용 가능한 API도 지원합니다.

Data Access 계층에서 JPA는 Service 계층과 바로 상호작용 하는 상단에 위치한다. 데이터 저장, 조회 등의 작업은 JPA를 거쳐 구현체인 Hibernate ORM을 통해 이루어지며 Hibernate ORM은 내부적으로 JDBC API를 이용해서 DB에 접근하게 된다.
Service 계층 ↔ Data Access 계층 [JPA | Hibernate ORM | JDBC API] ↔ DB

영속성 컨텍스트(Persistence Context)

테이블과 매핑되는 Entity 객체 정보를 영속성 컨텍스트에 보관해서 오래 지속되게 합니다.
이렇게 보관된 Entity 정보는 DB 테이블에 데이터를 insert, update, select, delete하는데 사용된다.
1차 캐시라는 영역과 쓰기 지연 SQL 저장소라는 영역이 있다.

엔티티 매핑

  • Entity(객체)와 테이블 매핑
@Entity
@Table
public class Memo {
    @Id
    private Long id;
}

@Entity : JPA 관리 대상 Entity가 된다.
@Entity(name = "MEMOS") : Entity 이름을 설정할 수 있다, name을 지정하지 않으면 기본값으로 클래스 이름을 Entity이름으로 사용

@Table : 옵션이다, @Table을 써주지 않으면 클래스 이름을 테이블 이름으로 사용(주로 Entity와 테이블 이름이 달라야 할 때 추가)
@Table(name = "MEMOS") : 테이블 이름을 설정할 수 있다, name을 지정하지 않으면 기본값으로 클래스 이름을 테이블 이름으로 사용

@Entity@Id는 필수이며 함께 사용해야한다.
@NoArgsConstructor 즉, 기본 생성자는 필수로 추가해야 한다, Spring Data JPA에서 기본 생성자가 없으면 에러가 발생하는 경우가 있다.


  • 기본키(PK) 매핑
    필드에 @Id를 추가하면 기본키(PK)가 된다.
    JPA에서는 기본키(식별자, PK) 생성 전략을 지원한다.
    기본키를 직접 할당하는 전략과 기본키를 자동 생성하는 전략이 있고 기본키 자동 생성 전략에는 IDENTITY, SEQUENCE, TABLE이 있다.

    • 기본키 직접 할당 전략

      • 코드에서 개발자가 기본키를 직접 써서 할당하는 방식
      • 필드에 @Id만 추가하면 기본키 직접 할당 전략 적용
      • new Memo(1L)과 같이 기본키를 직접 할당해서 Entity객체 생성
    • IDNEITY 전략

      • 기본키 생성을 DB에 위임하는 전략으로 DB에서 기본키를 대신 생성

      • AUTO_INCREMENT를 이용해 기본키 생성

      • 영속성 컨텍스트에서 DB로 commit전엔 기본키 값을 알 수 없음

        @NoArgsConstructor
         @Getter
         @Entity
         public class Memo {
             @Id
             @GeneratedValue(strategy = GenerationType.IDENTITY) // IDENTITY 전략
             private Long id;
        
             public Memo(Long id) { // 생성자
                 this.id = id;
             }
         }
    • SEQUENCE 전략

      • @GeneratedValue(strategy = GenerationType.SEQUENCE) 지정
      • DB의 시퀀스 이용
      • Entity가 영속성 컨텍스트에 저장되기 전에 DB가 시퀀스에서 기본키에 해당하는 값 제공 즉, commit 전에 확인 가능
    • AUTO 전략

      • @GeneratedValue(strategy = GenerationType.AUTO)
      • JPA가 DB에 따라 적절한 전략 자동 선택(mysql : auto_increment, oracle : sequence 등)

  • 필드(멤버 변수)와 컬럼 매핑
@Getter
@Setter
@NoArgsConstructor // 필수
@AllArgsConstructor // 테스트를 위해 추가됨
@Entity
public class ToDoList { // Entity
    @Id // 식별자 지정
    @GeneratedValue(strategy = GenerationType.IDENTITY) // 식별자 생성 전략 지정
    private Integer id;
    
    @Column(nullable = false)
    private String title;
    
    ...
}

@Column은 필드와 컬럼을 매핑해주는 애너테이션이다.
@Column생략 시 JPA는 기본적으로 해당 필드가 테이블의 컬럼과 매핑되는 필드라고 간주하며, @Column의 애트리뷰트 기본값이 모두 적용된다.

  • nullable
    • 컬럼에 null값을 허용할지 여부
    • 기본값 : true
      • 그러나 필드가 int나 long과 같은 기본형(primitive type)일 경우 @Column이 생략되면 기본적으로 nullable = false이다.
    • 즉, 필수 항목이면 nullable = false
  • updatable
    • 컬럼 값 수정 가능 여부
    • 기본값 : true
    • 즉, 수정 불가하면 updatable = false
  • unique
    • 컬럼에 유니크 제약 조건 설정
    • 기본값 : false
    • 즉, 고유한 값이면 unique = true

추가
@Transient : 필드에 추가하면 테이블 컬럼과 매핑하지 않겠다는 의미이다. 즉, DB에 저장 X


@Enumerated : enum 타입과 매핑

  • @Enumerated(EnumType.ORDINAL) : enum의 순서를 나타내는 숫자를 테이블에 저장
    • 기존 enum 사이에 새로 enum이 추가 되면, 테이블에 저장된 enum의 순서 번호와 실제 enum의 순서 번호가 일치하지 않게 된다.
  • @Enumerated(EnumType.STRING) : enum의 이름을 테이블에 저장
    • 권장

  • Entity 간의 연관 관계 매핑

repository.save()

public ToDoList createList(ToDoList toDoList) { 
        return listRepository.save(toDoList); 
    }
public ToDoList updateList(ToDoList toDoList) {
        ...
        return listRepository.save(findList);
    }

@Id 애너테이션이 추가된 엔티티 클래스의 멤버 변수 값이 0 또는 null이면 신규 데이터라고 판단하여 테이블에 insert 쿼리를 전송한다.
반면에 @Id 애너테이션이 추가된 엔티티 클래스의 멤버 변수 값이 0 또는 null이 아니라면 이미 테이블에 존재하는 데이터라고 판단하여 테이블에 update 쿼리를 전송한다.

쿼리 메서드(Query Method)
Spring Data JDBC 지원, Spring Data JPA 지원
find + By + SQL 쿼리문에서 WHERE 절의 컬럼명 + (WHERE 절 컬럼의 조건 데이터) 형식으로 쿼리 메서드를 정의하면 조건에 맞는 데이터를 테이블에서 조회한다.
ex) Optional<엔티티> findByListId(Integer listId);
listId컬럼을 WHERE절의 조건으로 지정해서 해당 엔티티 테이블에서 하나의 row를 정의한다.
즉, SELECT * FROM 엔티티 WHERE LIST_ID = ?으로 DB의 해당 엔티티 테이블에 질의를 보낸다.
SQL 질의를 통한 결과 데이터를 엔티티 클래스의 객체로 리턴한다.


WHERE 절의 조건 컬럼을 여러 개 지정하는 방법 : And 사용
ex) findByEmailAndName(String email, String name)


※주의
WHERE 절의 컬럼명은 내부적으로 테이블의 컬럼명으로 변경되지만 실질적으론 엔티티 클래스로 작업을 하기 때문에 엔티티 클래스의 멤버 변수명을 적어야 한다.

Optional
Spring Data JDBC 지원, Spring Data JPA 지원
Optional<엔티티> findByEmail(String email);는 리턴값을 Optional로 래핑한 것이다.
JDK1.8부터 생기면서 null 값 처리에 편리해서 많이 쓴다.


추가중...

틀린 부분 댓글 지적 환영합니다

profile
개인 공부 기록장

0개의 댓글