[인프런 워밍업 클럽 스터디1기] 백엔드 - 2주차 회고

[인프런 워밍업 클럽 스터디1기] 백엔드 - 2주차 회고

2주차 정리 및 회고

Section3. 역할의 분리와 스프링 컨테이너

클린코드

클린코드 : 함수는 최대한 작게 만들고 한 가지 일만 수행하는 것이 좋다.

 

UserController.java

  • api 진입점,

  • 현재 유저가 있는지 없는지 확인하고 예외처리,

  • SQL을 사용해 실제 database와의 통신

     

     

    을 담당하는 3가지 역할을 다 하고 있으므로, 클린코드가 아니다. 이러한 상태의 단점은

 

  • 너무 큰 기능이기 때문에 테스트도 힘들다.

  • 종합적으로 유지보수성이 매우 떨어진다.

따라서 Controller를 3단 분리하여 클린 코드로 작성하였다.

 

기존 api 진입점으로써 HTTP Body를 객체로 변환의 역할은 Controller 의 역할로 남겨두고,

현재 유저가 있는지 없는지 확인하고 예외처리 의 역할은 Service가,

SQL을 사용해 실제 database와의 통신 의 역할은 Repository가 담당한다.

 

스프링 컨테이너

서버가 시작되면, 스프링 컨테이너(클래스 저장소)가 시작된다. 스프링 빈들(클래스들) 이 등록되고 - dependency 주입된, 사용자가 직접 설정해준 스프링 빈이 등록된다. 이때 필요한 의존성이 자동으로 설정된다.

예를 들어, UserController에서 필요한 JdbcTemplate이 자동으로 생성자 내로 들어간다.

 

image위와 같이 Book 관련 3단 분리 코드를 예시로 봤다.

이때, 두 repository 중 어떤 것을 우선순위로 하는지는 @Primary @Qualifier 어노테이션을 사용하면 된다.

 

Section4. 생애 최초 JPA 사용하기

JPA 사용

지금까지 작성한 코드를 살펴보면 아쉬운 몇 가지가 있다.

  • repository 클래스 내에서 문자열로 쿼리를 작성하기 때문에 실수할 수 있고, 실수를 인지하는 시점이 느리다.

  • 특정 데이터베이스에 종속적이게 된다. 우리의 경우엔 MySql

  • 반복 작업이 많아진다.

  • 데이터베이스의 테이블과 객체의 매핑되는 패러다임이 다르다.

image

따라서 JPA (Java Persistence API) 데이터를 영구적으로 보관하기 위해 java 진영에서 정해진 규칙을 사용한다.

즉, 객체와 관계형 데이터베이스 테이블을 짝지어 데이터를 영구적으로 저장할 수 있도록 돕는다.

이를 사용하기 위해서는 Hibernate 가 필요하다.

image직접 매핑해보자

유저 테이블에 대응되는 entity class 인 User.java 를 만들면 다음과 같이 코드가 수정된다.

package com.group.libraryapp.domain;

import org.springframework.lang.Nullable;

import javax.persistence.*;

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id = null;
    @Column(nullable = false, length = 20, name = "name" ) //name varchar(20)
    private String name;
    @Column(nullable = false)
    private Integer age;

    protected User() {
    }

    public User(String name, Integer age) {
        if (name == null || name.isBlank()){
            throw new IllegalArgumentException(String.format("잘못된 name(%s)이 들어왔습니다."));
        }
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public Integer getAge() {
        return age;
    }

    public Long getId() {
        return id;
    }

    public void updateName(String name) {
        this.name = name;
    }
}

또한 jpa 를 사용하기 위해서 application.yml 파일도 변경하자.

hibernate 부분을 추가해준다.

spring:
  datasource:
    url: "jdbc:mysql://localhost/library"
    username: "root"
    password: "1234"
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    hibernate:
      ddl-auto: none
    properties:
      hibernate:
        show_sql : true
        format_sql : true
        dialect : org.hibernate.dialect.MySQL8Dialect

😀 spring.jpa.hibrenate.ddl-auto : none

스프링이 시작할 때 DB에 있는 테이블을 어떻게 처리할지에 대한 옵션이다. 현재 DB에 테이블이 잘 만들어져 있고, 미리 넣어둔 데이터도 있으므로 별 다른 조치를 하지 않는다.

 

자동으로 쿼리 날리기 (기본)

repository 폴더 내에 UserRepository interface를 만들어준 뒤,

JpaRepository 를 상속받게 해준다.

package com.group.libraryapp.repository.user;

import com.group.libraryapp.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByName(String name);

}

UserService.java에서

save, findAll, delete 등의 기본적인 쿼리들은 sql 문자열을 타이핑할 필요없이 자동으로

날릴 수 있게 되었다.

package com.group.libraryapp.service.user;

import com.group.libraryapp.domain.User;
import com.group.libraryapp.repository.user.UserRepository;
import com.group.libraryapp.dto.User.request.UserCreateRequest;
import com.group.libraryapp.dto.User.request.UserUpdateRequest;
import com.group.libraryapp.dto.User.response.UserResponse;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.stream.Collectors;

@Service
public class UserServiceV2 {
    private final UserRepository userRepository;

    public UserServiceV2(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public void saveUser(UserCreateRequest request){
        userRepository.save(new User(request.getName(),request.getAge()));
    }// INSERT SQL 이 자동으로 날라감.

    public List<UserResponse> getUser(){
        return userRepository.findAll().stream()
                .map(UserResponse::new )
                .collect(Collectors.toList());
    }

    public void updateUser(UserUpdateRequest request){
       User user =  userRepository.findById(request.getId())
                .orElseThrow(IllegalArgumentException::new);

       user.updateName(request.getName());
       userRepository.save(user);
    }

    public void deleteUser(String name){
    User user = userRepository.findByName(name)
            .orElseThrow(IllegalArgumentException::new);

    userRepository.delete(user);
    }


}

JpaRepository<Entity, ID>를 구현 받는 Repository에 대해 자동으로 기본적인 메소드(save, findAll) 를 사용할 수 있는 SimpleJpaRepository 기능을 사용할 수 있게 해준다.

 

그렇다면 다른 다양한 쿼리는 어떻게 작성할까?

위 코드에서 삭제 기능인 deleteUser 메소드는

userRepository.findByName 메소드를 사용하고 있다.

이 메소드는 SimpleJpaRepository 기능이 아니라, 추가로 interface에 추상(?) 함수 정의만 해놓은 findByName 을 사용한 것이다.

 

By 앞에는 다음과 같은 구절이 들어갈 수 있다.

  1.  find

  2. findAll

  3. exists

  4. count

By 뒤에는 필드 이름이 들어가는데, And나 Or 로 조합될 수 있다.

 

또한 동등조건 (=) 외에도 다양한 조건을 사용할 수 있다.

image

회고

벌써 워밍업 클럽 스터디의 반이나 지났다. 확실히 이렇게 약간의 강제성을 부여하는 스터디가 나랑 잘 맞는 것 같다. 나 혼자서 공부했으면, 금방 흐지부지 되었을텐데 스터디 코치님들이 디스코드에 종종 올려주시는 글들이나, 다른 분들이 과제한 부분들을 읽으면서 배우는 점도 많은 것 같고, 완주러너의 혜택도 욕심이 나서 더욱 공부를 하게 되는 것 같다.

댓글을 작성해보세요.

채널톡 아이콘