인프런 워밍업 클럽 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를 붙여 스프링이 자동으로 해당 레포로 선택하게 하면 된다!
스프링 컨테이너를 다루는 방법
빈을 등록하는 방법
@Configuration
클래스에 붙이는 어노테이션
@Bean을 사용할 때 함께 사용해 주어야 한다.
@Bean
메소드에 붙이는 어노테이션
메소드에서 반환되는 객체를 스프링 빈에 등록한다.
UserRepository.java에 있는 @Repository 어노테이션 삭제 후 @Bean으로 등록
언제 @Service, @Repository를 사용해야하나??
→ 개발자가 직접 만든 클래스를 스프링 빈으로 사용할 때
@Configuration, @Bean는 언제 사용?
→ 외부 라이브러리, 프레임워크에서 만든 클래스를 등록할 때
@Component
주어진 클래스를 ‘컴포넌트’로 간주한다.
이 클래스들은 스프링 서버가 뜰 때 자동으로 감지된다.
→ 사실 숨겨져 있었다.. 타고 들어가면 사용되고 있는것을 확인할 수 있다.
→ 컨트롤러, 서비스, 리포지토리가 모두 아니고, 개발자가 직접 작성한 클래스를 스프링 빈으로 등록할 때 사용되기도 한다.
스프링 빈을 주입 받는 몇 가지 방법
생성자를 이용해 주입받는 방식 (가장 권장)
기존에는 @Autowired 어노테이션을 붙여야했는데 스프링 버전이 업데이트 되면서 안붙여도 자동으로 등록되게 되었다.
setter와 @Autowired 사용 → setter를 사용할 경우 오류발생 확률 높아짐
필드에 직접 @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.java를 UserJdbcRepository.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;
트랜잭션 적용과 영속성 컨텍스트
우리가 원하는 것은
서비스 메소드가 시작할 때 트랜잭션이 시작되어
서비스 메소드 로직이 모두 정상적으로 성공하면 commit 되고
서비스 메소드 로직 실행 도중 문제가 생기면 rollback 되는 것
@Transactional 어노테이션으로 우리가 원하는 것을 할 수 있다!
SELECT 쿼리만 사용할경우, readOnly 옵션을 사용하여 데이터 변경을 위한 기능이 빠져 약간의 성능 향상이 있다.
영속성 컨텍스트란?
테이블과 매핑된 Entity 객체를 관리/보관하는 역할
⇒ 스피링에서는 트랜잭션을 사용하면 영속성 컨텍스트가 생겨나고, 트랜잭션이 종료되면 영속성 컨텍스트가 종료된다.
변경 감지 (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메소드를 작성해주지 않아도 된다.
쓰기 지연
: DB의 INSERT / UPDATE / DELETE SQL을 바로 날리는 것이 아니라, 트랜잭션이 commit될 때 모아서 한 번만 날린다.
예시로 유저가 저장되는 부분이 여러개 일때, 하나씩 날려서 저장하는것이 아니라 우선 기억해두고 한번에 저장하여 DB에 보내게 된다.
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문으로 변경하였다..!
금요일에 깜짝 라이브를 참석하지 못해서 너무 아쉬웠지만 따로 내용을 올려주셔서 참고할 수 있었다 : )
댓글을 작성해보세요.