인프런워밍업스터디_BE_7일차_수업내용정리

5hseok·2024년 2월 23일
0

지금까지는 Repository에서 SQL문을 사용하여 MySQL과 상호작용했다. 그러나, SQL문을 직접 작성해서 DB에 접근하는 것은 아쉬운 점이 많다.

  1. 문자열을 작성할 때 실수가 발생할 수 있고, 이 실수를 인지하기 어렵다.
  • SQL문이 잘못 작성된 경우는 컴파일 단계에서 에러가 나지 않고, 잘못 작성된 코드가 실행될 때, 즉 런타임 단계에서 에러가 발생하기 때문에 실수를 찾기 어렵다.
  1. 특정 DB에 종속적이게 된다.
  • DB마다의 SQL문이 다르기 때문에, DB를 바꾸게 된다면 모든 코드에서의 SQL 문자열을 바꿔줘야 한다.
  1. 반복 작업이 많아진다.
  • 테이블을 하나 만들 때마다 CRUD 쿼리를 항상 만들어줘야 한다.
  1. 객체와 DB의 테이블은 패러다임이 다르다.
  • 데이터베이스의 테이블
    관계형 데이터 모델에서는 데이터를 테이블의 형태로 저장한다. 테이블은 행과 열로 구성되며, 각 행은 고유한 식별자를 가지고 있다. 테이블 간의 관계는 외래 키를 통해 연결된다. 예를 들어, 학생과 과목이라는 두 개의 테이블이 있다고 가정하면, 학생 테이블에는 학생의 이름, 학번 등의 정보가 있고, 과목 테이블에는 과목명, 과목 코드 등의 정보가 있다. 학생이 수강하는 과목을 나타내려면, 학생 테이블과 과목 테이블 사이에 수강 과목이라는 새로운 테이블을 만들고, 이 테이블에 학생의 학번과 과목 코드를 외래 키로 저장하여 두 테이블을 연결해야 한다.

  • 프로그래밍 언어의 객체
    객체 지향 모델에서는 데이터와 데이터를 처리하는 메소드를 하나의 객체로 묶어서 다룬다. 객체는 클래스를 통해 정의되며, 클래스는 객체의 속성과 메소드를 정의한다. 객체 간의 관계는 주로 상속, 합성, 연관 등의 방식으로 표현된다. 예를 들어, 학생과 과목을 클래스로 표현한다면, 학생 클래스에는 이름, 학번 등의 속성과 수강 신청, 수강 취소 등의 메소드가 있을 수 있고, 과목 클래스에는 과목명, 과목 코드 등의 속성과 수강생 추가, 수강생 제거 등의 메소드가 있을 수 있다. 학생이 수강하는 과목을 나타내려면, 학생 클래스에 수강 과목 리스트를 속성으로 추가하고, 이 리스트에 수강하는 과목 객체를 추가하거나 제거하는 메소드를 정의할 수 있다.

    이렇게 보면, 관계형 데이터 모델은 데이터의 구조와 관계에 초점을 맞추고, 객체 지향 모델은 데이터와 데이터를 처리하는 행동을 하나의 객체로 묶는 것에 초점을 맞추기 때문에, 데이터베이스의 테이블과 프로그래밍 언어의 객체를 연결하는 것이 쉽지 않다.

JPA

이런 문제를 해결하기 위해 도입된 개념이 JPA이다. JPA(Java Persistence API)는 데이터를 영구적으로 저장하기 위한 자바 진영에서 정한 규칙으로 해석할 수 있다. 여기서의 Persistence가 영속성이란 뜻이다.

또한, JPA는 객체와 관계형 DB를 짝지어서 동작하는데, 이를 다시 말하면 자바 진영에서의 ORM이라고 할 수 있다.

파이썬 진영에서의 ORM이 Model을 사용한
Django ORM이었던 것처럼 자바 진영에서의 ORM은 JPA인 것이다.

하지만, JPA는 말 그대로 API, 즉 규칙이기 때문에 이 규칙에 맞춰진 코드를 사용해야 한다. 이 코드가 바로 Hibernate이다. 마치 JPA는 인터페이스와 같은 역할의 규칙이고, 이를 구현한 구현체가 Hibernate인 것이다.

쉽게 설명하자면, GET, POST 등은 HTTP 프로토콜에서 정의한 메소드이다. 이 메소드들은 클라이언트가 서버에게 어떤 동작을 요청할 지를 나타내는 메소드인데, 이 메소드들로만은 아무 동작이 안되고, 실제로 이 GET, POST 요청에 대한 처리를 코드로 구현한 곳은 웹 서버나 웹 애플리케이션인 것처럼 JPA와 Hibernate의 관계를 정의할 수 있다.

또한, 이 Hibernate는 내부적으로 JDBC를 사용하여 JPA를 구현했다. 그래서 이 관계를 그림으로 표현하면 다음과 같다.

24강

우선 user 테이블과 매핑되는 객체를 만들어야 한다. 이 객체들은 @Entity라는 어노테이션이 붙는다. Entity는 '저장되고, 관리되어야 하는 데이터'로 스프링이 이를 인식하여 서버가 동작할 때, User 객체와 user 테이블을 같은 것으로 간주하게 된다.

현재 user 테이블
    CREATE TABLE user(
    id bigint auto_increment,
    name VARCHAR(20),
    age int,
    primary key (id)
);
현재 User 객체 필드
public class user {
    private final String name;
    private final Integer age;
	...

user 테이블과 User 객체는 name과 age가 동일하고, 테이블에는 객체에 없는 id Column이 존재한다.

우선 User클래스에 @Entity를 붙이고 User 객체에 id 필드를 추가해준다. 그 후, id 필드 위에 @Id를 붙여 이 필드가 Id를 나타낸다는 표시를 해두고, @GeneratedValue(strategy = GenerationType.IDENTITY) 어노테이션을 붙인다.
@GeneratedValue는 DB에서 자동으로 생성해줬던 Primary Key를 객체에서 만들기 위함이다. 이 strategy는 DB마다 다른데, MySQL은 IDENTITY를 사용한다.

혹시 @Entity나 다른 어노테이션이 안 나온다면, build.gradle에서 
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
작성하여 JPA 의존성 설정하기

이후, User 클래스에 기본 생성자(파라미터를 가지지 않는)가 꼭 들어가야 하므로 새로 만들어준다. 이 생성자는 protected여도 괜찮다.

다음으로 기본적인 Column에도 어노테이션이 존재한다. 필드 위에 @Column(...)을 붙이는건데, 이 어노테이션은 null 가능 여부, 길이 제한, DB에서의 Column 이름을 설정할 수 있다.

ex) name VARCHAR(20)
 -> @Column(nullable = false, length = 20, name="name")
    private String name;

이때 name에 해당하는 "name"이 필드명과 동일하다면 생략이 가능하다.

또한, @Column 자체를 생략할 수도 있다.

ex) age int
 -> private Integer age;

이 age의 경우에는 객체에서의 필드 선언과 DB에서의 Column속성이 같기 때문에 @Column을 굳이 써주지 않아도 된다.

이렇게 하면 모든 매핑이 끝나게 된다. 마지막으로 application.yml에 최초로 JPA에 적용할 때 설정해주는 옵션을 추가한다.

spring:
  jpa:
    hibernate:
      ddl-auto: none
    properties:
      hibernate:
        format_sql: true
        show_sql: true
        dialect: org.hibernate.dialect.MySql8Dialect
spring.jpa.hibernate.ddl-auto

스프링이 시작할 때 DB에 있는 테이블을 어떻게 처리할지에 대한 옵션이다.
create : 기존 테이블이 있다면 삭제 후 다시 생성한다.
create-drop : 스프링이 종료될 때 테이블을 삭제한다.
update : 객체와 테이블이 다른 부분만 변경한다.
validate : 객체와 테이블이 동일한지 확인한다.
none : 별다른 조치를 하지 않는다.

만약 create나 create-drop을 사용한다면 DB가 메모리처럼 서버가 종료될 때 전부 삭제된다. validate같은 경우는 확인 후 false라면 서버를 종료시킨다. 우리는 테이블과 객체를 완전히 매핑했고, 미리 넣어둔 데이터도 존재하기 때문에 none으로 설정했다.

spring.jpa.properties.hibernate.format_sql

JPA를 사용해 DB에 SQL을 날릴 때 SQL을 예쁘게 포맷팅할지 결정한다.

spring.jpa.properties.hibernate.show_sql

JPA를 사용해 DB에 SQL을 날릴 때 SQL을 보여줄지 결정한다.

spring.jpa.properties.hibernate.dialect

dialect(사투리, 방언) 옵션을 통해 JPA가 알아서 Database끼리 다른 SQL을 조금씩 수정해 준다. 현재 사용 중인 DB가 MySQL 8버전이므로 MySQL8Dialect로 설정했다.

JPA를 활용하여 User API 리팩토링

  1. User 도메인 객체와 같은 위치(domain 패키지)에 UserRepository라는 인터페이스를 만들고 기존 UserRepository의 이름은 UserJdbcRepository로 변경한다.
  2. 이후, UserRepository가 JpaRepository를 상속받도록 한다. 이때, 우리가 만든 테이블을 매핑한 객체인 User와 이 테이블의 id 타입을 전달해줘야 한다.
public interface UserRepository extends JpaRepository<User, Long> {
}
  1. UserService도 UserServiceV1으로 이름을 바꾸고 JPA를 활용할 UserServiceV2를 새로 만든다.

저장 기능

@Service
public class UserServiceV2 {
    private final UserRepository userRepository;

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

    public void saveUser(UserCreateRequest request){
        //save 메소드에 객체를 넣어주면 INSERT SQL이 자동으로 날아간다.
        userRepository.save(
        new User(request.getName(), request.getAge())
        );
    }

UserJdbcRepository를 의존하고 있던 Service가 새로 만든 UserRepository에 의존하게 만든다. 이때, UserRepository 인터페이스는 extends JpaRepository에 의해 자동으로 스프링 빈으로 등록되게 된다.
똑같이 생성자를 만들어서 의존성을 받아주고, saveUser 메소드를 만든다.

saveUser 메소드는 UserCreateRequest를 받아 userRepository.save()안으로 들어가게 된다.
save()는 들어온 객체를 저장하거나 업데이트 시켜주는 메소드이다. 그래서 save()안에 User 객체를 만들어 request값을 전달해주면 DB로 INSERT SQL이 자동으로 날아가는 것이다. 이 반환값은 참고로 User 객체이다.

조회 기능

    public List<UserResponse> getUser(){
        //findall 메소드를 쓰면 모든 데이터를 가져온다.
        return  userRepository.findAll().stream()
                .map(user->(new UserResponse(user.getId(), user.getName(), user.getAge())))
                .collect(Collectors.toList());
    }

getUser()는 전체 User의 정보를 가져오는 메소드이다. 이 메소드의 구현을 위해 findAll()을 사용했다. findAll()은 주어진 객체가 매핑된 테이블의 모든 데이터를 가져와 객체를 담은 리스트로 반환한다.

이걸 UserResponse로 가공하기 위해 stream으로 만들고, UserResponse에 매핑한 후, 다시 List로 바꾸어 return 한다.

.map(user->(new UserResponse(user.getId(), user.getName(), user.getAge())))
// 람다식을 활용해서 map한 이 코드는 
// UserResponse에 User을 인자로 받는 생성자를 추가해서 
// 이렇게 간소화할 수 있다.
.map(UserResponse::new)

업데이트 기능

업데이트는 두 가지 기능을 사용한다.

  1. id를 받아 이 id에 맞는 User가 있는지 확인하고
  2. id에 맞는 User가 있다면 User의 정보를 수정한다.
    public void updateUser(UserUpdateRequest request){
        //select * from user where id = ?
        //결과 : Optional<User>
        User user = userRepository.findById(request.getId())
                .orElseThrow(IllegalArgumentException::new);
        user.updateName(request.getName());
        userRepository.save(user);
    }

findById()를 사용해서 수정할 수 있다. findById()는 id를 기준으로 특정한 데이터 1개를 가져온다. 이 반환값은 Optional<객체>이다. 이 점을 이용해 .orElseThrow를 사용해서 findById()가 null값이라면 IllegalArgumentException 에러를 발생하게 했다. 이 결과를 user로 받는데, 이 user는 request로 들어온 id를 가진 User 객체이다. 이후, user이름을 request에서 들어온 이름으로 바꾸고, save()로 객체를 업데이트 시킨다.
updateName()은 User 객체에 새로 만들어준다.

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

메소드 정리

이렇게 SQL을 작성하지 않아도 쿼리가 자동으로 나가는 기능을 우리는 Spring Data JPA를 사용하여 구현한 것이다. Spring Data JPA는 build.gradle에 의존성을 주입할 때 나왔던 키워드인데, 이 Spring Data JPA는 복잡한 코드를 우리가 쉽게 사용할 수 있게 해준다.

우리는 Spring Data JPA를 바로 .save, .findAll과 같이 사용하고 있지만, 이 Spring Data JPA 내부에는 JPA에 맞게 구현한 Hibernate가 있고 이 안에는 또 Jdbc가 있다. 결국 시간이 흐르면서 DB에 쉽게 접근할 수 있는 방법으로 고안되서 널리 사용되는 것이 바로 지금 했던 Spring Data JPA가 되겠다.

26강

남은 유저 삭제 기능을 구현해보겠다.

삭제 기능

    public void deleteUser(String name){

        User user = userRepository.findByName(name);
        if (user == null){
            throw new IllegalArgumentException();
        }
        userRepository.delete(user);
    }

삭제 기능은 id가 아니라 name으로 User를 찾아서 name과 일치하는 User를 삭제하게 된다. 그러나, JPA에서 findById()는 기본적으로 제공했지만, findByName()이라는 메소드는 없었다.

이럴 때는 SQL문을 조합하는 것처럼 우리가 메소드를 만들면 된다. 정확히는 메소드의 이름을 만드는 것이다.
UserRepository 인터페이스에 findByName(String name)을 추가하고 난 후, userRepository.findByName(name)을 사용하면 에러가 나지 않는다!

어떻게 된 것일까?
Spring Data JPA에서는 메소드의 이름만 잘 작성하면 SQL이 조립된다.

  • find : 1개의 데이터를 가져온다.
  • By : WHERE과 같은 의미로 By 뒤에 붙는 필드 이름으로 SQL문이 완성된다.
  • ex) findByName = select * from user where name = ?

find의 반환값은 객체 혹은 null값이다. 이를 이용하여 name값이 일치하는 객체가 있는지 확인한 후, delete()를 이용해서 객체를 삭제해줬다.

이처럼 메소드의 이름을 지어 SQL문을 조립할 수 있다.

By 앞에 붙는 구절

By 뒤에 붙는 필드

ex) 특정 나이 사이의 유저를 검색할 때
    -> findAllByAgeBetween(int startAge, int endAge)

0개의 댓글

관련 채용 정보