인프런 워밍업 클럽 BE 1기 - 2주차 발자국

인프런 워밍업 클럽 BE 1기 - 2주차 발자국

UserController의 의아한점

  • static이 아닌 코드를 사용하려면 인스턴스화 (new)가 필요하다

  • UserController를 인스턴스화하고 있지 않은데 누가 하고 있는것인가???

  • UserController는 JdbcTemplate에 의존하고 있다.

     

    그런데 JdbcTemplate란 클래스도 설정해준적이 없다..! 어떻게 가져온거지?

⇒ @RestController가 다해주고 있었다!!!!!

⇒ UserController클래스를 API의 진입지점으로 만들 뿐 아니라 UserController 클래스를 스프링 빈으로 등록 시킨다.

 

스프링 빈(Bean)이란?

  • 서버가 시작되면, 스프링 서버 내부에 거대한 컨테이너를 만들게 되고, 컨테이너 안에는 클래스가 들어가게 된다.!

    이때 다양한 정보도 함께 들어있고, 인스턴스화도 이루어진다.

  • JdbcTemplate도 스프링 빈에 등로되어 있었기에 사용할 수 있었다. (dependencies로 의존)

UserRepository는 JdbcTemplate을 가져오지 못할까?

  • JdbcTemplate을 가져오려면 UserRepository가 스프링 빈이어야 하는데 UserRepository는 스프링 빈이 아니다!!

    → UserRepository를 스프링 빈으로 등록하자!

스프링 컨테이너를 왜 사용할까?

메모리에 저장하는 레포를 만든다고 가정하고 코딩…

→ MySQL로 저장하고 싶은데?

→ 관련된 모든 것들을 다 MySQL로 변경하는 레포로 변경해야함

!!! 데이터를 메모리에 저장할지, MySQL에 저장할지 Repository의 역할에 관련된 것만 바꾸고 싶은데 BookService까지 바꿔야 한다. (OMG)

⇒ Java의 interface를 활용하자!

그래도 Service를 수정해야하는 상황이 발생..

 

스프링 컨테이너를 사용하면?

BookMemoryRepository, BookMySqlRepository 둘 중 BookService에 쓰일 것을 컨테이너가 선택한다!!

→ 이런 방식을 제어의 역전(IoC, Inversion of Control)이라 한다.

→ 컨테이너가 선택해 BookService에 넣어주는 과정을 의존성 주입(DI, Dependency Injection)라고 한다.

@Primary를 붙여주면 해당 어노테이션이 있는 곳이 사용된다. (우선권 결정)

⇒ 나중에 바꿔야 Memory에서 mysql로 바꾸는 작업이 있어서 코드를 다 바꿔야 할 경우 @Primary를 붙여 스프링이 자동으로 해당 레포로 선택하게 하면 된다!

 

스프링 컨테이너를 다루는 방법

  1. 빈을 등록하는 방법

    • @Configuration

      • 클래스에 붙이는 어노테이션

      • @Bean을 사용할 때 함께 사용해 주어야 한다.

    • @Bean

      • 메소드에 붙이는 어노테이션

      • 메소드에서 반환되는 객체를 스프링 빈에 등록한다.

      UserRepository.java에 있는 @Repository 어노테이션 삭제 후 @Bean으로 등록

언제 @Service, @Repository를 사용해야하나??

→ 개발자가 직접 만든 클래스를 스프링 빈으로 사용할 때

@Configuration, @Bean는 언제 사용?

→ 외부 라이브러리, 프레임워크에서 만든 클래스를 등록할 때

@Component

  • 주어진 클래스를 ‘컴포넌트’로 간주한다.

  • 이 클래스들은 스프링 서버가 뜰 때 자동으로 감지된다.

→ 사실 숨겨져 있었다.. 타고 들어가면 사용되고 있는것을 확인할 수 있다.

→ 컨트롤러, 서비스, 리포지토리가 모두 아니고, 개발자가 직접 작성한 클래스를 스프링 빈으로 등록할 때 사용되기도 한다.

 

스프링 빈을 주입 받는 몇 가지 방법

  1. 생성자를 이용해 주입받는 방식 (가장 권장)

    기존에는 @Autowired 어노테이션을 붙여야했는데 스프링 버전이 업데이트 되면서 안붙여도 자동으로 등록되게 되었다.

  2. setter와 @Autowired 사용 → setter를 사용할 경우 오류발생 확률 높아짐

  3. 필드에 직접 @Autowired 사용 → 테스트하기 어렵다.

@Qualifier

: 여러개의 후보군이 있을때 그 중 하나를 특정해서 가져올 수 있게 끔 한다.

  • 스프링 빈을 사용하는 쪽, 스프링 빈을 등록하는 쪽 모두 @Qualifier를 사용할 수 있다.

  • 스프링 빈을 사용하는 쪽에서만 쓰며, 빈의 이름을 적어주어야 한다.

  • 양쪽 모두 사용하면, @Qualifier끼리 연결된다.

@Primary vs @Qualifier → 동시에 사용할 경우 어떤게 우선일까?

  • 사용하는 쪽에서 직접 적어준 @Qualifier가 이긴다.

SQL을 직접 작성하게되면..

  • SQL을 직접 작성할 경우 오류가 나도 확인하기 힘들다

    ⇒ 컴파일 시점에 발견되지 않고 런타임 시점(서버가 이미 가동된 후)에 발견된다

  • 특정 데이터베이스에 종속적이게 된다.

    ⇒ 다른 DB를 사용할경우 다 바꿔줘야 한다.

  • 반복 작업이 많아진다. 테이블을 하나 만들 대마다 CRUD쿼리가 항상 필요하다.

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

→ JPA( Java Persistence API) 등장!

: 객체와 관계형 DB의 테이블을 짝지어 데이터를 영구적으로 저장할 수 있도록 정해진 Java 진영의 규칙

 

유저테이블에 대응되는 Entity Class 만들기

@Entity 어노테이션을 User 클래스에 붙인다.

→ @Entity : 스프링이 User객체와 user 테이블을 같은 것으로 바라본다. (저장되고, 관리되어야 하는 데이터)

우선 테이블의 PK인 id를 만들어보자

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

@Id : 이 필드를 primary key로 간주한다.

@GeneratedValue : primary key는 자동 생성되는 값이다.

(strategy = GenerationType.IDENTITY)

여기 부분은 DB종류마다 자동생성 전략이 다르다!! 주의!!

MySQL의 auto_increment를 사용했기에 IDENTITY를 매칭

@Entity를 사용할 경우 - 매개변수가 하나도 없는 기본 생성자가 꼭 필요하기 때문에 다음 코드를 작성!

protected User() {}

@Column : 객체의 필드와 Table의 필드를 매핑한다.

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

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

null이 들어갈 수 없거, 길이는 20자, 필드의 이름은 name (이 경우는 name = “name” 동일하기에 생략 가능)

 

@Column 은 생략가능하기 때문에 널이 들어갈 수 있고 굳이 특정할필요 없다면 생략이 가능하다.

 

JPA 설정 추가 - application.yml

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

ddl-auto : 스프링이 시작할 때 DB에 있는 테이블을 어떻게 처리할지

none 외에 다른 종류

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

  • create-drop : 스프링이 종료될 때 테이블을 모두 제거 -… 사용 조심할것 ! 데이터가 모두 날라갈 수있음.

  • update : 객체와 테이블이 다른 부분만 변경

  • validate : 객체와 테이블이 동일한지 확인

  • none : 별다른 조치를 하지 않는다.

show_sql : JPA를 사용해 DB에 SQL을 날릴 때 SQL을 보여줄 것인가

format_sql : SQL을 보여줄 때 예쁘게 포맷팅 할 것인가

dialect : 방언, 사투리.. → 이 옵션으로 DB를 특정하면 조금씩 다른 SQL을 수정해준다.

 

Spring Date JPA를 이용하여 자동으로 쿼리 날리기

기존 UserRepository.javaUserJdbcRepository.java로 변경하고

domain > user > UserRepository 인터페이스를 생성하자

생성 후 JpaRespository를 상속 받는다.

public interface UserRepository extends JpaRepository<User, Long> {}

<User, Long> → User의 primary key인 Id의 타입이 Long이라 Long으로 작성한다.

 

유저 데이터 정보 추가하기 (INSERT)

public void saveUser(UserCreateRequest request) 
{ userRepository.save(new User(request.getName(), request.getAge())); }

JpaRepository를 상속 받는 userRepository에 save메소드를 객체에 넣어주면 INSERT SQL이 자동으로 날아간다. → save되고 난 후의 User는 id가 들어 있다

유저 정보 조회하기 (SELECT)

public List<UserResponse> getUserList(){
    return userRepository.findAll().stream()
        .map(user -> new UserResponse(user.getId(), user.getName(), user.getAge()))
        .collect(Collectors.toList());
}

findAll() : 해당 테이블의 모든 데이터를 조회한다.

user를 stream으로 mapping시켜주고 user에 new UserResponse로 id, name, age를 넣어준다.

그 후 결과를 List로 반환

유저 정보 조회 후 수정하기 (UPDATE)

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

findById를 사용하면 id를 기준으로 1개의 데이터를 가져온다.

.orElseThrow(IllegalArgumentException::new);

→ Optional의 orElseThrow를 사용해 User가 없다면 예외를 던진다. 있을경우 user에 담긴다

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

도메인 user의 updateName메소드에 변경할 reqest.getName을 인자로 넣어 이름을 변경해주고

save를 통해 user의 정보를 저장한다. (자동으로 UPDATE SQL이 날라가게 된다.)

 

어떻게 SQL을 작성하지 않아도 동작하지? JPA인가?

→ Spring Data JPA가 도와준다.

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

 

By앞에 들어갈 수 있는 구절 정리

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

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

  • exists : 쿼리 결과가 존재하는지 확인. 반환 타입은 boolean

  • count : SQL의 결과 개수 반환타입은 long

By뒤에 들어갈 수 있는 구절 정리

  • GreaterThan : 초과

  • GreaterThanEqual : 이상

  • LessThan : 미만

  • LessThanEqual : 이하

  • Between : 사이에

    • SELECT * FROM user WHERE age BETWEEN ? AND ?;

    List<User> findAllByAgeBetween(int startAge, int endAge);
  • StartsWith : ~로 시작하는

  • EndsWith : ~로 끝나는

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

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

트랜잭션 시작하기

start transaction;

트랜잭션 정상 종료하기 (SQL 반영)

commit;

트랜잭션 실패처리(SQL 미반영)

rollback;

 

트랜잭션 적용과 영속성 컨텍스트

우리가 원하는 것은

  1. 서비스 메소드가 시작할 때 트랜잭션이 시작되어

  2. 서비스 메소드 로직이 모두 정상적으로 성공하면 commit 되고

  3. 서비스 메소드 로직 실행 도중 문제가 생기면 rollback 되는 것

@Transactional 어노테이션으로 우리가 원하는 것을 할 수 있다!

SELECT 쿼리만 사용할경우, readOnly 옵션을 사용하여 데이터 변경을 위한 기능이 빠져 약간의 성능 향상이 있다.

 

영속성 컨텍스트란?

테이블과 매핑된 Entity 객체를 관리/보관하는 역할

⇒ 스피링에서는 트랜잭션을 사용하면 영속성 컨텍스트가 생겨나고, 트랜잭션이 종료되면 영속성 컨텍스트가 종료된다.

  1. 변경 감지 (Dirty Check)

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

    @Transactional
        public void updateUser(UserUpdateRequest request) {
            User user = userRepository.findById(request.getId())
                    .orElseThrow(IllegalArgumentException::new);
            user.updateName(request.getName());
            //userRepository.save(user);
        }

    유저의 정보가 업데이트가 되었네? 바뀌었구나? 하고 자동으로 저장된다. 따라서 save메소드를 작성해주지 않아도 된다.

  2. 쓰기 지연

    : DB의 INSERT / UPDATE / DELETE SQL을 바로 날리는 것이 아니라, 트랜잭션이 commit될 때 모아서 한 번만 날린다.

    예시로 유저가 저장되는 부분이 여러개 일때, 하나씩 날려서 저장하는것이 아니라 우선 기억해두고 한번에 저장하여 DB에 보내게 된다.

     

  3. 1차 캐싱

    : ID를 기준으로 Entity를 기억한다.

    쓰기 지연과 비슷하게 이전에 캐싱된 객체를 기억하고 있다가 동일한 값이면 DB에 계속해서 요청하지 않고 알려준다.

책 생성 API 개발하기

create table book (id bigint auto_increment, name varchar(255), primary key(id));

name varchar(255)을 사용한 이유?

  • @Column의 length 기본값이 255

  • 문자열 필드는 최적화를 해야 하는 경우가 아닐 때 조금 여유롭게 설정하는 것이 좋다.


회고

4번째과제는 과일가게 판매에 관련된 내용이었다.

삼단분리 후 뭔가 직접 구현해봐야하는 문제라 그런지 사실 과제를 하면서 재밌게 느껴졌다.

단순히 요구사항에 나와있는데로만 작성하다보니 id를 왜 long 타입으로 써야했을까? 등 '왜' 라는 생각을 잘 하지 못했는데 앞으로는 왜? 의문점을 가지고 생각해봐야겠다!!

5번째 과제에선 클린코드에 나와있는 내용을 바탕으로 더 좋은 코드로 변경해보는 과제이었다.

문제를 보고 우선 각각의 기능을 하는 함수로 나누어 분리해야겠다는 생각이 들었다. 또 if-else문을 지양해야한다고 했지만, 너무 많은 조건을 가지고 있는 if-else문의 경우에는 사용을 하는것이 오히려 나을수도 있다는 블로그 글을 참고하여

조건부분을 따로 함수로 만들어 그 함수를 호출하는 if-else문을 생각하고 1차 리팩토링 하였다.

하지만 그래도 if-else문에는 너무 반복되는 부분이 많았고 이부분을 결국 for문으로 변경하였다..!

금요일에 깜짝 라이브를 참석하지 못해서 너무 아쉬웠지만 따로 내용을 올려주셔서 참고할 수 있었다 : )

댓글을 작성해보세요.