[인프런 워밍업 클럽 BE]6-8일차 강의 정리

이 문서는

자바와 스프링 부트로 생애 최초 서버 만들기, 누구나 쉽게 개발부터 배포까지! [서버 개발 올인원 패키지] 강의 - 인프런 (inflearn.com)

강의와 연관 스터디 내용을 정리하였습니다. 

 

6일차.

 

19강.

왜 UserController에 JdbcTemplate을 인스턴스하지 않았는데 평범하게 실행이 되었을까?

 

인스턴스 클래스에 소속된 개별적 객체 Person 클래스와 Job 클래스가 별도로 있을 때, Person 클래스 안에 Job a = new Job(); 형식으로 작성하는 것을 인스턴스화, Person 클래스에 개별적인 객체가 되는 것이다. 물론 static 객체는 생성없이 이용가능하다.

 

어디선가 자동으로 인스턴스화해준다는 의미일텐데... UserContoller에서 JdbcTemplate은 필수적으로 필요하다.

= UserContoller은 JdbcTemplate에 의존

이는 @RestController가 해결하고 있다.

→ @RestController는 UserController를 자동으로 스프링빈으로 등록한다.

 

스프링 빈

서버가 시작되면, 스프링 서버 내부에 거대한 컨테이너가 생성되는데, 이 때 이 스프링 컨테이너가 관리하는 객체를 스프링 빈이라고 한다.

 

스프링 컨테이너

스프링 프레임워크의 핵심 컴포넌트.

객체의 생명 주기 관리 및 추가적인 기능 제공

  • 빈의 인스턴스화, 구성, 생명 주기, 제거

  • 의존성 주입(DI)

  • 다른 빈끼리의 연결

사실 JdbcTemplate은 이미 스프링빈으로 등록되어 있다!

설정한 dependency(spring JPA)가 자동 등록하고 있다.

스프링 컨테이너가 서로 필요한 클래스끼리 연결해주고 있다!

그럼 왜 Repository에는 굳이 JdbcTemplate을 인스턴스화했을까?

⇒ Repository가 빈이 아니기 때문!

@Repository라는 어노테이션을 붙여 빈으로 등록하자!

 

마찬가지로 Service에도 @Service를 붙이자.

@RestController
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }
}
@Service
public class UserService {
    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}
@Repository
public class UserRepository {
    private final JdbcTemplate jdbcTemplate;

    public UserRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }
}

 

20강.

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

책 이름을 메모리에 저장하는 API를 구현해보자. 이 때 서비스와 레포지토리는 스프링 빈이 아니어야 한다.

BookController → BookService → BookMemoryRepsitory

 

그런데… 갑자기 클라이언트가 DB에 저장하도록 요구사항을 변경했다!

BookController → BookService → BookMysqlRepsitory

 

이렇게 되면, 레포지토리를 변경했을 뿐인데 서비스 코드도 변경해야 한다. 만약 BookMysqlRepsitory를 쓰는 서비스 클래스가 몇 십, 몇 백 개 라면…? 문제가 점점 커진다.

자바 인터페이스를 사용은 어떨까. 구현체 부분만 바꿀 수 있다고는 하지만, 어차피 서비스 코드를 손 본다는 것은 바뀌지 않는다.

이제 스프링 컨테이너를 사용한다고 해보자.

컨테이너는 BookMemoryRepsitory BookMysqlRepsitory를 모두 빈에 넣어두고, 선택해서 자동으로 인스턴스화해줄 수 있다!

스프링이 알아서 컨트롤해준다!

제어의 역전(IoC)

컨테이너가 자동으로 선택해 인스턴스화 해주는 과정

의존성 주입(DI)

@Service
public class BookService {
//    private final BookMemoryRepository bookMemoryRepository = new BookMemoryRepository();
    private final BookRepository bookRepository;

    public BookService(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }
}
@Repository
public class BookMysqlRepository implements BookRepository{
    @Override
    public void saveBook() {

    }
}
@Repository
public class BookMemoryRepository implements BookRepository{
    @Override
    public void saveBook() {

    }
}

BookService와 BookMysqlRepository, BookMemoryRepository 을 모두 빈으로 등록해줬다.

이제 서비스에 손을 대지 않은 채 레포지토리를 변경할 수 있다! 근데… 무슨 수로, 무슨 기준으로 선택할 수 있을까? 같은 @Repository 가 붙어있는데 말이다.

스프링 컨테이너도 이런 상황을 보면 에러를 던진다! 우선 순위를 줘야한다. 사용할 스프링 위에 @Primary를 붙이자.

@Primary - 빈의 우선권 결정

@Primary
@Repository
public class BookMysqlRepository implements BookRepository{
    @Override
    public void saveBook() {

    }
}

 

21강.

빈 등록 방법

@Configuration

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

  • @Bean 사용 시 함께 사용해야 한다.

@Bean

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

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

@Repository에 주석처리를 한 후, 직접 빈 등록을 해보자.

@Configuration
public class UserConfiguration {
    @Bean
    public UserRepository userRepository(JdbcTemplate jdbcTemplate){
        return new UserRepository(jdbcTemplate);
    }
}

이렇게 하면 UserService에서도 오류없이 주입하는 것을 볼 수 있다.

언제 @Service, @Repository 사용?

  • 개발자가 직접 만든 클래스에 사용

  • @Configuration+@Bean 은 외부 라이브러리, 프레임워크에서 만든 클래스를 등록할 때 사용한다.

    • ex) JdbcTemplate

@Component

  • 주어진 클래스를 컴포넌트로 간주

  • 해당 클래스들은 스프링 서버가 실행할 때 자동 감지 후 스프링 컨테이너에 들어와, 스프링 빈으로 등록

  • @Service, @Repository, @Controller 등 이미 여러 어노테이션에 붙어있다!(자동 감지 중~)

  • 컨트롤러, 서비스, 리포지토리가 아닌 개발자가 직접 작성한 클래스를 스프링 빈으로 등록할 때 사용

스프링 빈 주입 방법

  1. 생성자 <- 가장 권장

    private final JdbcTemplate jdbcTemplate;
    
        public UserRepository(JdbcTemplate jdbcTemplate) {
            this.jdbcTemplate = jdbcTemplate;
        }
    

     

  2. Setter + @Autowired

    private  JdbcTemplate jdbcTemplate;
        @Autowired
        public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
            this.jdbcTemplate = jdbcTemplate;
        }
    

    누군가 setter에 접근해 문제가 발생할 가능성

     

     

  3. 필드에 직접 @Autowired 사용

    @Autowired
    private  JdbcTemplate jdbcTemplate;
    

    테스트가 어려워지는 요인

     

     

    @Qualifier

    @Primary처럼 여러 후보군 중 사용할 빈을 선택

    @Qualifier와 @Primary를 동시에 사용한다면?

    @Qualifier 등록!(사용자가 직접 연결해주었다는 특수함)

     


7일차.

 

23강.

SQL 문 직접 작성의 문제

  1. 문자열 작성이라 오타가 나기 쉽고, 발견이 어려움

    → 컴파일 시점에 발견 X, 런타임 시점에 발견

  2. 특정 DB에 종속된다.

    →DB마다 문법이 조금씩 달라 오류 발생 위험

  3. 반복 작업이 많다. 테이블 하나당 CRUD쿼리 반복 작성 필요.

  4. DB의 테이블과 객체는 패러다임이 다르다.

    1. 교실과 학생들 객체가 있다고 가정

    2. 교실에는 학생들을 담는 리스트가 존재한다.

    3. 학생 역시 본인이 속한 교실에 대한 정보를 담기 위해 교실 객체를 갖고 있다.

       

    4. 부모 클래스, 하위 클래스 역시 DB로 표현이 어렵다.

⇒ JPA(Java Persistence API) 사용으로 이를 해결

 

JPA(Java Persistence API)

  • 자바 진영의 ORM(Object-Relational Mapping)

  • Persistence : 영속성

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

  • ORM - 객체와 DB테이블을 매핑

⇒ 객체와 관계형 DB 테이블을 짝지어 데이터를 영구적 저장하도록 해주는 JAVA 진영 규칙

HIBERNATE

  • 규칙을 코드로 구현, 작성

  • HIBERNATE는 JPA를 구현하고 있는 구현체이다.

  • 내부적으로 Jdbc 사용


24강.

매핑 방법

User객체와 user 테이블을 매핑

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id=null;

		...
}

위 형태로 수정

@Entity - 스프링이 User 객체와 user 테이블을 같은 것으로 간주

Entity - 저장되고, 관리되어야 하는 데이터

@Id - 해당 필드를 PK로 간주

@GeneratedValue - 자동 생성되는 PK

strategy = GenerationType.IDENTITY - mysql의 auto_increment와 동일. oracle dbms의 경우 주로 SEQUENCE를 선택

Entity 객체는 매개변수가 없는 기본 생성자가 필요하다. protected여도 무방.

protected User() {}
@Column(name = "name", nullable = false, length = 20)
    private String name;

@Column

  • 객체의 필드와 테이블의 필드 매핑

  • 객체 필드 이름과 테이블 필드 이름이 동일하다면 name=”” 생략

  • 완전 동일한 제약조건인 경우에는 @Column 생략 가능

 

JPA 사용을 위한 설정

yml 파일로 돌아가자.

spring:
  datasource:
    url: "jdbc:mysql://localhost/library"
    username: root
    password: 
    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

ddl-auto - 스프링 시작 시 DB 테이블 처리 방법

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

  • create-drop: 스프링 종료 시 테이블 모두 제거

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

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

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

show_sql - JPA를 사용에 DB에 SQL을 날릴 때 SQL을 보여주는 것에 대한 여부

format-sql - SQL을 보여줄 때 예쁘게 포맷팅 여부

dialect - 하나의 DB로 특정하면 조금씩 다른 문법을 수정

 

25강.

JPA를 이용한 자동 쿼리

이전 유저 생성/조회/업데이트 기능 리팩토링

이전의 UserRepository는 UserJdbcRepository로 이름 변경만 한 후,

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

위 파일 생성

JpaRepository<1, 2>

  • 1 - 엔티티 객체

  • 2 - 해당 객체의 기본키 타입

저장

userRepository.save(new User(request.getName(), request.getAge()));

전체조회

        List<User> all = userRepository.findAll();
//        return all.stream().map(user -> new UserResponse(user.getId(),user.getName(),user.getAge())).collect(Collectors.toList());
        return all.stream().map(UserResponse::new).collect(Collectors.toList());

주석처리된 부분과 그 하단 코드는 동일한 기능이다. 코드 단축을 위해 UserResponse에 User 가 매개변수인 생성자를 별도 생성해 작성한 것이다.

조건조회

//반환은 Optional<User>
userRepository.findById(request.getId()) 

수정

User user = userRepository.findById(request.getId())
                .orElseThrow(IllegalArgumentException::new); //해당 유저가 존재하지 않는다면 오류를 던진다.
        user.updateName(request.getName()); //domain에 있는 User 파일에 updateName이란 메서드 생성
        userRepository.save(user); //변경사항 감지 후 저장

JPA가 어떻게 쿼리를 처리?

⇒ Spring Date JPA가 처리

Spring Date JPA - 복잡한 JPA코드를 스프링과 함께 쉽게 사용하도록 도와주는 라이브러리(Simple Jpa Repository)

26강.

삭제

User user = userRepository.findById(request.getId())
                .orElseThrow(IllegalArgumentException::new); //해당 유저가 존재하지 않는다면 오류를 던진다.
userRepository.delete(user);

만약 id 기준으로 찾는 것 외에, 다른 쿼리를 만들고 싶다면?

JpaRepository를 상속받은 UserRepository에 가서 함수(메서드 내부 코드X)를 추가!

함수 이름에 맞춰, sql이 자동 작성된다.

find - 1개의 데이터만 가져온다.

By - By 다음에 오는 필드명으로 ‘where filed_name=?’ 인 조건문이 붙는다.

JPA 사용할 수 있는 각종 키워드

find: 1건을 가져온다. 반환 타입은 객체 혹은 Oprional<객체/타입>

findAll: 쿼리 결과가 N개, List<객체/타입> 반환

exists: 쿼리 결과 존재 확인. boolean 반환

count: sql의 결과 개수. long 반환

By~

Filed1AndFiled2: 논리곱 연산자

Filed1OrFiled2: 논리합 연산자

Filed1GreaterThan: 초과

Filed1GreaterThanEqual: 이상

Filed1LessThan: 미만

Filed1LessThanEqual: 이하

Filed1Between: 사이에

Filed1StartsWith: ~로 시작하는

Filed1EndsWith: ~로 끝나는

 

8일차.

 

27강

트랜잭션

  • DB의 논리적 연산 단위. 1개의 트랜잭션에는 N개(≥1)의 SQL문 포함.

  • 분할할 수 없는 최소 단위. 전부 적용하거나 전부 취소하거나.

트랜잭션 관련 sql문

start transaction; //시작
commit; //commit
rollback; //rollback

 

28강

JPA에서의 트랜잭션 적용

⇒Service 빈에서 하나의 트랜잭션으로 묶을 로직 메서드 위에 @Transcational 을 추가한다.

@Transactional
    public void saveUser(UserSaveRequest request){
        User u = userRepository.save(new User(request.getName(), request.getAge()));
    }

 

@Transcational

  • 붙어있는 메서드가 시작할 때 트랜잭션을 시작

  • 예외 없이 잘 끝났다면 commit

  • 오류라면 rollback

     

조회 쿼리만 사용하는 경우 옵션으로 readOnly = true 를 붙여주자. 약간의 성능이 향상된다.

@Transactional(readOnly = true)
public List<UserResponse> getUsers(){}

 

IOException과 같은 Checked Exception은 기본적으로 롤백이 일어나지 않는다.

=> 옵션으로 롤백 처리를 할 수 있다.

 

영속성 컨텍스트

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

스프링에서는 트랜잭션 사용 시 영속성 컨텍스트가 생성

트랜잭션 종료 시 영속성 컨텍스트가 종료

 

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

  • 변경 감지(Dirty Check)

    • 영속성 컨텍스트 안에서 불러온 엔티티는 명시적으로 save하지 않더라도 변경 감지 후 자동 저장

    • ex) 이전에 만든 updateUser 메서드에서 userJPARepository.save를 사용하지 않아도 자동 업데이트해준다.

  • 쓰기 지연

    • DB의 입력/수정/삭제 sql을 바로 날리지 않고 트랜잭션이 commit될 때 모아서 한 번만 날린다.

    • DB와의 통신 횟수가 줄어드는 효과

  • 1차 캐싱

    • ID를 기준으로 엔티티를 기억

    • DB와의 통신 횟수가 줄어드는 효과

    • 캐싱된 객체는 완전 동일(객체 인스턴스 동일!)

       

채널톡 아이콘