🤍 전 강의 25% 할인 중 🤍

2024년 상반기를 돌아보고 하반기에도 함께 성장해요!
인프런이 준비한 25% 할인 받으러 가기 >>

[인프런 워밍업 클럽 스터디]BE 1기 세번째 발자국

[인프런 워밍업 클럽 스터디]BE 1기 세번째 발자국

어째 계속 변명 회고가 되어가고 있는데 호기롭게 다짐한 것과 달리 공부를 많이 못했다. 지금 이후부터 담주까지 열심히 강의를 들어 완주하는 것에 만족하려고 한다. ㅜㅜ 아직도 10일차에 머물러 있지만 금방 완주할 수 있을거다!!

 


Section4

섹션4에서는 JPA를 다루는 방법에 대해 배운다.

 

문자열 SQL을 직접 사용하는 것의 한계

  1. 문자열을 작성하기 때문에 실수할 수 있고, 실수를 인지하는 시점이 느림

        // 유저 조회
        public List<UserResponse> getUsers() {
            String sql = "SELECT * FROM user";
            return jdbcTemplate.query(sql, (rs, rowNum) -> {
                long id = rs.getLong("id");
                String name = rs.getString("name");
                int age = rs.getInt("age");
                return new UserResponse(id, name, age);
            });
        }
    

    String sql = "SELECT * FROM user"; 이 부분에 실행이 되지 않아도 빨간줄이 안 뜸

    ⇒ 오류가 컴파일 시점에 발견되 지 않고, 런타임 시점에 발견됨

     

    ( 컴파일 : 서버를 실행할때 자바 코드를 .class 코드로 변화시켜서 jvm에서 동작시켜야하는 과정)

     

    ( 런타임 : 실제 서버가 가동된 이후)

  2. 특정 데이터베이스에 종석적이게 됨

    1. 다른 db로 바꾸려면 전체를 바꿔야 하는 어려움

  3. 반복 작업이 많아짐, 테이블을 하나 만들 때마다 CRUD 쿼리가 항상 필요

  4. 데이터베이스의 테이블과 객체는 패러다임이 다름

    1. 부모 클래스와 하위 클래스들을 구분짓기 어려움

JPA란?

Java Persistence API ( 자바 영속성 API ) ⇒ 자바 진영의 ORM(Object - Relational Mapping)

Java Persistence API ( 자바 영속성 API )

영속성 : 서버가 재시작되어도 데이터는 영구적으로 저장되는 속성

API : 정해진 규칙

⇒ 데이터를 영구적으로 보관하기 위해 자바 진영에서 정해진 규칙

 

ORM(Object - Relational Mapping)

Object : 객체

Relational : 관계형 db

⇒ 관계형 db의 테이블

Mapping : 객체랑 테이블을 연결

 

즉, JPA란 ?

객체와 관계형 db의 테이블을 짝지어 데이터를 영구적으로 보관하기 위해 Java 진영에서 정해진 규칙

말로 되어있는 규칙을 코드로 표현 → Hibernate

Hibernate(구현체) → 구현(implement) → JPA

 

⇒ 즉, JPA는 interface(규칙)이고, Hibernate는 그것을 구현하는 구현체며 JDBC를 사용한다.

 


다음 강의에서는 User 객체에 엔티티를 넣는 방법을 배운다.

사실 객체에 @Entity 를 적어주면 된다.

@Entity
public class User {

    @Id // id라는 걸 알리기 위해 => primary
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id = null;

    @Column(nullable = false, length = 20) // name(varchar) = 20
    private String name;
    private Integer age;

}

@Entity : 스프링이 User객체와 user 테이블을 같은 것으로 바라봄

entity의 의미 ⇒ 데이터베이스에서 저장되고, 관리되어야 하는 데이터

 

id에 auto_increment 가 있으므로

@GeneratedValue(strategy = GenerationType.*IDENTITY*) 로 자동 생성되는 값임을 명시

@id : 이 필드를 primary key로 간주함

@GeneratedValue : primary key는 자동 생성되는 값임

 

@Column : 객체의 필드와 Table의 필드를 매핑함

null이 들어갈 수 있는지의 여부, 길이 제한, db에서의 column 이름 등등이 들어감

 

매핑은 끝 → JPA를 사용하니 추가적인 설정을 해주어야 함

application.yml

  jpa: # 
    hibernate:
      ddl-auto: none # 우리 프로젝트는 테이블과 객체가 잘 연결되었기 때문에 필요 X
    properties:
      hibernate:
        show_sql: true # JPA를 사용해 db에 Sql을 날릴 때 Sql을 보여줄지에 관한 것
        format_sql: true # sql을 보여줄 때 예쁘게 포맷할 것인지
        dialect: org.hibernate.dialect.MySQL8Dialect # 이 옵션으로 db를 특정하면 조금씩 다른 Sql을 수정

Spring에 jpa-hobernate-ddl-auto라는 옵션이 있는데 이 옵션은 스프링이 시작할때 디비에 있는 테이블을 어떻게 처리할지에 관함

→ 테이블이랑 객체랑 다름(매핑을 잘못함)일 때 어떻게 할지에 대한 옵션

ddl-auto: create : 기존 테이블이 있다면 삭제 후 다시 생성

ddl-auto: create-drop : 스프링이 종료될 때 매핑됐던 테이블 모두 제거

ddl-auto: update : 객체와 테이블이 다른 부분만 변경

ddl-auto:validate : 객체와 테이블이 동일한지 확인

ddl-auto: none : 별다른 조치 X

 


Spring Data JPA를 이용해 자동으로 쿼리 날리는 방법에 대해 배운다.

⇒ sql을 작성하지 않고 유저 생성/ 조회/ 업데이트 기능 리팩토링

 

유저 생성 기능

UserServiceV2

@Service
public class UserServiceV2 {
    private final UserRepository userRepository;

    public UserServiceV2(UserRepository userRepository) {
        this.userRepository = userRepository;
    } // 이미 UserRepository에서 JPA를 상속받기 때문에 그냥 불러와서

    public void saveUser(UserCreateRequest request){
        userRepository.save(new User(request.getName(), request.getAge()));
    } // save 함수만 써주면 저장됨 => 이미 있는 문법(sql문이 필요없다)
    // User -> JPA로 되어있는 객체
}

save 메소드에 객체를 넣어주면 INSERT SQL이 자동으로 날라감

save되고 난 후 유저는 id를 갖게 됨

 

유저 조회 기능

유저 조회 기능

UserServiceV2

// 유저 조회
    public List<UserResponse> getUsers(){
        return userRepository.findAll().stream() // 모든 테이블을 가져옴
        .map(user -> new UserResponse(user.getId(), user.getName(), user.getAge()))
        .collect(Collectors.toList());
    }

자바 문법을 이용해 더 간단하게 변경 가능(자바 8버전을 이용한 stream)

stream 으로 만들어서 mapping 후 유저리스트로 다시 만듦 .collect(Collectors.toList());

⇒ 즉 유저 객체를 가져와서 객체 간의 변환하게 됨

findAll() : 모든 데이터를 가져옴(select * from user;)

 

유저 업데이트 기능

  1. id를 이용해 User을 가져와 User가 있는지 없는지 확인하고

  2. User가 있다면 update 쿼리를 날려 데이터 수정(없으면 예외 던짐)

UserServiceV2

// 유저 업데이트
    public void updateUser(UserUpdateRequest request){
        // 원하는 sql : select * from where id = ?;
        // 반환값 : Optional<User> => 매핑했던 user 객체
        User user = userRepository.findById(request.getId())
                .orElseThrow(IllegalArgumentException::new); // 유저가 없다면 예외, 유저가 있다면 결과값 들어옴
        user.updateName(request.getName()); // 유저의 객체를 가져와 업데이트 
        // request.getName() -> 변경되어야 하는 api의 이름
        userRepository.save(user); // 자동으로 user의 이름이 바뀌어있는걸 확인하고 바뀐걸 기준으로 업데이트 쿼리가 날아감
    }

findById : id를 기준으로 조회

 

지금까지 사용한 기능

save : 주어지는 객체를 저장하거나 업데이트 시켜줌

findAll : 주어지는 객체가 매핑된 테이블의 모든데이터 가져옴

findById : id를 기준으로 특정한 1개의 데이터 가져옴

어떻게 Sql을 작성하지 않아도 동작하는걸까?!

JPA가 자동으로 처리하는겅가? 반은 맞고 반은 틀림

Spring Data JPA 가 하는 것임 ( Spring Data JPA ≠ JPA)

Spring Data JPA : 복잡한 JPA 코드를 스프링과 함께 쉽게 사용할 수 있도록 도와주는 라이브러리

 


유저 삭제 기능

⇒ spring JPA로 바꾸기

삭제는 이름 기준인데 이름 기준 조회는 없음

💡 어떻게 select * from user where name = ? 을 어떻게 만들 수 있을까?

UserRepository interface에 들어가서 유저 반환 함수 만들기

User entity 객체에 있음

UserRepository

public interface UserRepository extends JpaRepository<User, Long> {
    User findByname(String name); // -> select * from user where name = ?;
}

반환 타입은 User, 유저가 없다면 null 반환

함수 이름을 작성하면, 알아서 sql 조회되는 방식임 ⇒ 함수 이름을 기본 함수처럼 만들어야 함

find 라고 작성하면, 1개의 데이터만 조회

By 뒤에 붙는 필드 이름으로 SELECT 쿼리의 where 문이 작성됨

UserServiceV2

// 유저 삭제
    public void deleteUser(String name){
        User user = userRepository.findByname(name);
        if (user == null) {
            throw new IllegalArgumentException("User not found");
        }
        userRepository.delete(user); // 무조건 유저가 존재함을 의미(없으면 예외처리가 되니깐)
    }

delete : 주어지는 데이터를 db에서 제거함

 

다양한 Spring Data JPA 쿼리에는 무엇이 있을까?

 

  • By 앞에 들어갈 수 있는 기능

find : 1건을 가져옴. 반환 타입은 객체가 될 수도 있고, Optional<type>이 될 수도 있음

findAll : 쿼리의 결과물이 N개인 경우 사용. List<타입> 반환

exist : 쿼리 결과가 존재하는지 확인. 반환 타입은 boolean (존재하면 true, 존재 X false)

count : sql의 결과 개수를 셈. 반환 타입은 long (ex. long countByAge)

 

  • By 뒤에 들어갈 수 있는 기능 ( findByName )

각 구절은 AndOr 로 조합할 수 있음

List<User> findAllByNameAndAge(string name, int age);

=> SELECT * FROM user WHERE name =? AND age = ?;

GreaterThan : 초과 ( 필드명 뒤에)

GreaterThanEqual : 이상

LessThan : 미만

LessThanEqual : 이하

Between : 사이에

 


트랜잭션

트랜잭션 : 쪼갤 수 없는 업무의 최소 단위

 

만약 쇼핑몰 사이트에서 물건을 주문하면

  1. 주문 기록 저장

  2. 포인트 저장

  3. 결제 기록 저장

public class OrderService {
  public void completePayment() {
    orderRepository.save(new Order(..));
    pointRepository.save(new Point(..));
    billingHistoryRepository.save(new BillingHistory(..));
  }
}

billingHistoryRepository.save(new BillingHistory(..)); 에서 만약 에러가 난다면?

⇒ 주문 정보 데이터가 있고 포인트 저장 데이터가 있는데 결제 기록에는 안 뜨게 된다

pointRepository.save(new Point(..)); 에서 만약 에러가 난다면?

⇒ 주문 기록은 있는데 포인트와 결제 기록이 없다

모든 sql을 성공시키거나, 하나라도 실패하면 모두 실패시키자!

⇒ 쪼갤 수 없고 한 몸이다 ⇒ 트랜잭션

 

트랜잭션 명령어

start transaction; // 트랜잭션 시작
commit; // 정상 종료
rollback; // 실패 처리

성공 처리시키면 보이고(commit) 실패 처리 시키면 안 보임(rollback)

우리의 코드에 어떻게 적용할 수 있을까?

public class OrderService {
  public void completePayment() {
    orderRepository.save(new Order(..));
    pointRepository.save(new Point(..));
    billingHistoryRepository.save(new BillingHistory(..));
  }
}

completePayment() 함수가 시작되기 전에 트랜잭션을 시작하고

모두 성공하면 commit 하나라도 실패하면 rollback을 시키자

우리 프로젝트에서 트랜잭션을 사용하려면

@Transactional 사용해 적용시키면 된다.

 

UserServiceV2

@Service
public class UserServiceV2 {
    private final UserRepository userRepository;

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

    // 유저 생성
    @Transactional
    public void saveUser(UserCreateRequest request){
        userRepository.save(new User(request.getName(), request.getAge()));
    }

    // 유저 조회
    @Transactional
    public List<UserResponse> getUsers(){
        return userRepository.findAll().stream()
                .map(UserResponse::new)
                .collect(Collectors.toList());
    }

    // 유저 업데이트
    @Transactional
    public void updateUser(UserUpdateRequest request){
        // 원하는 sql : select * from where id = ?;
        // 반환값 : Optional<User>
        User user = userRepository.findById(request.getId())
                .orElseThrow(IllegalArgumentException::new); // 유저가 없다면 예외, 유저가 있다면 결과값 들어오고
        user.updateName(request.getName()); // 들어온 이름
        userRepository.save(user); // 자동으로 user의 이름이 바뀌어있는걸 확인하고 바뀐걸 기준으로 업데이트 쿼리가 날아감
    }

    // 유저 삭제
    @Transactional
    public void deleteUser(String name){
        User user = userRepository.findByname(name).orElseThrow(IllegalArgumentException::new);
        
        userRepository.delete(user); // 무조건 유저가 존재함을 의미
    }
}

SELECT 쿼리만 사용한다면, readOnly 옵션을 쓸 수 있음

트랜잭션을 사용했을 때 데이터 변경을 할 수 없음 ⇒ 보통 조회에 사용

 

영속성 컨텍스트 : 테이블과 매핑된 Entity 객체를 관리/보관하는 역할

User.java

@Entity // 엔티티
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id = null;

    @Column(nullable = false, length = 20) // name(varchar) = 20
    private String name;
    private Integer age;
    
    // id, name, age => 테이블관 매핑된 객체

스프링에서는 트랜잭션을 사용하면 영속성 컨텍스트가 생겨나고, 트랜잭션이 종료되면 영속성 컨텍스트가 종료됨. ⇒ 즉 트랜잭션과 함께 생겨나고 트랜잭션과 함께 종료됨

 

영속성 컨텍스트의 특수 능력 4가지

  1. 변경 감지(Dirty Check)

     

    : 영속성 컨텍스트 안에서 불러와진 Entity는 명시적으로 save 하지 않더라도, 변경을 감지해 자동으로 저장됨

  2. 쓰기 지연

    DB의 insert / update / delete sql을 바로 날리는 것이 아니라 트랜잭션이 commit될 때 모아서 한 번에 날림

     

    1. 쓰기 지연이 없다면? ⇒ A, B, C를 각자 저장 → spring과 db 사이의 통신이 3번 일어남(시간이 오래 걸림)

    2. 쓰기 지연이 있다면? ⇒ 3번 통신했지만 한 번에 저장하기(시간이 빠름)

  3. 1차 캐싱

    1. 영속성 컨텍스트가 없다면 3번 통신이 일어남

      첫번째 코드가 실행되면 조회 (영속성 컨텍스트가 id가 1인 유저 기억)

       

    2. → 두번째 코드가 실행되면 id가 1인 유저를 이미 알고 있어서 가지고 있는 정보를 내보냄


과제 6. Layered Architecture

https://velog.io/@dmsqls19/인프런-워밍업-클럽-1기-BE-과제6.-기능-분리

 


회고

이 이후 섹션부터는 지금 공부 중에 있다. 아무래도 내 다짐과는 다르게 너무 공부를 덜 한 느낌이라 시간이 된다면 완주한 후에 발자국을 남겨보도록 하겠다. 사실 강의 정리는 계속 강의를 들으면서 따로 정리하고 있기 때문에 나중에 다 완강하고 한 번 더 들으면서 제대로 공부해 볼 생각이다. 이번주는 과제도 한 개 밖에 제출하지 못 했다. 여러모로 아쉬운 한 주다ㅠㅜ

 

https://quartz-mastodon-ac1.notion.site/6ae20b419ca9400c94b4df9ab8f2f3da?pvs=74

댓글을 작성해보세요.

채널톡 아이콘