[인프런 워밍업 스터디 클럽 1기] 2주차 발자국
발자국
강의는 진도표에 맞춰 진행하였으며 회고록 또한 그에 맞춰 작성하였다.
인프런 워밍업 스터디 클럽 완주를 목표로 남은 기간동안 매일매일 꾸준히 학습하기 위해 노력하겠다.
2주차 학습 내용
DAY6 : 스프링 컨테이너의 의미와 사용 방법
19강. UserController와 스프링 컨테이너
UserController의 의아한 점
static이 아닌 코드를 사용하려면 인스턴스화 필요
인스터스화를 하기 위해선 생성자를 호출하게 된다.
하지만 이전 강의까지 new UserController 이렇게 호출한 적이 없다!
UserController는 JdbcTemplate에게 의존하고 있다.
하지만 JdbcTemplate이라는 클래스를 직접 설정해준 적이 없다!
→ 그 이유는 @RestController 어노테이션에게 있다. → SpringBean으로 등록해준다!
SpringBean
서버가 시작되면, 스프링 서버 내부에 거대한 컨테이너를 만든다.
컨테이너 안에 여러 클래스가 들어간다.
이때, 다양한 정보도 함께 들어있고, 인스턴스화도 이루어 진다.
스프링 빈이란? 컨테이너 안에 들어간 클래스를 말한다.
UserController와 다르게 JdbcTemplate은 언제 스프링 빈으로 등록하였을까?
→ 의존성 설정을 할 당시에 스프링 빈으로 등록된다.
스프링 컨테이너의 역할은 서로 필요한 관계에 있는 스프링 빈끼지 연결을 시켜주는 역할이다.
이론 정리
서버가 시작되면 스프링 컨테이너(클래스 저장고)가 시작된다.
기본적으로 많은 스프링 빈들이 등록된다.
우리가 설정해준 스프링 빈이 등록된다.
이때 필요한 의존성이 자동으로 설정된다.
왜?
그렇다면 UserRepository는 JdbcTemplate을 가져오지 못할까?
→ UserRepository가 스프링 빈이 아니기 때문이다.
실습
UserService와 UserRepository를 스프링 빈으로 등록해준 뒤 UserRepository에게만 JdbcTemplate 필요하게 되었고 controller는 service만을 호출하고 service는 repository만을 호출하여 사용할 수 있게 된다.
실습상황
실습한 코드의 서버가 시작하면,
가장 기본적인 스프링 빈이 등록된다.
JdbcTemplate을 의존하는 UserRepository가 스프링 빈으로 등록되면서 인스턴스화된다.
UserRepository를 의존하는 UserService가 스프링 빈으로 등록된다.
UserService를 의존하는 UserController가 스프링 빈으로 등록된다.
20강. 스프링 컨테이너를 왜 사용할까?
다음 요구사항을 생각해보자
책 이름을 메모리에 저장하는 API를 매우 간단하게 구현하라. Service, Repository는 Spring Bean이 아니어야 한다.
구현된 그림
BookController —> BookService —> BookMemoryRepository
실습진행
스프링 빈을 설정하지 않고 메모리에 저장한다는 가정하에 실습 진행
추가 요구사항 구현
메모리가 아닌 MySQL과 같은 DB를 사용해야 한다. JdbcTemplate은 Repository가 바로 설정할 수 있다고 해보자.
구현된 그림
BookController —> BookService BookMemoryRepository —> BookMysqlRepository
하지만 Repository 뿐만 아니라 Service까지 바꿔야 한다!
이런 상황에서 Repository를 다른 Class로 바꾸더라도 Service를 변경하지 않는 방법은? → Interface를 활용하자
수정된 구현
BookController —> BookService —> BookRepository ← BookMemoryRepository ← BookMysqlRepository
실습진행
인터페이스 생성하여 레포지토리가 바뀌더라고 쉽게 사용할 수 있게 실습 진행
2개의 레포지토리로 인해 에러가 발생하는데 사용하는 레포지토리에 @Primary 어노테이션을 활용해서 조절하면 에러가 해결된다.
하지만 Service의 변경 범위가 줄어들었지만 아쉬운 부분이 있다.
그래서 등장한 것이 스프링 컨테이너이다.
컨테이너를 사용하면, 컨테이너가 Service를 대신 인스턴스화 하고 그 때 알아서 Repository를 결정해준다.
이런 방식을 제어의 역전(IoC, Inversion of Control)이라고 한다.
컨테이너가 선택해 Service에 넣어주는 과정을 의존성 주입(DI, Dependency Injection)라고 한다.
21강. 스프링 컨테이너를 다루는 방법
빈을 등록하는 방법
@Configuration
클래스에 붙이는 어노테이션
@Bean을 사용할 때 함께 사용해 주어야 한다.
@Bean
메소드에 붙이는 어노테이션
메소드에서 반환되는 객체를 스프링 빈에 등록한다.
실습진행
UserRepository에 Bean를 사용해보자
@Configuration public class UserConfiguration { @Bean public UserRepository userRepository(JdbcTemplate jdbcTemplate) { return new UserRepository(jdbcTemplate); } }
언제 @Service, @Repository를 사용해야 할까?
개발자가 직접 만든 클래스를 스프링 빈으로 등록할 때
사실 위의 실습은 @Repository를 사용하는 것이 관례이다.
언제 @Configuration + @Bean을 사용해야 할까?
외부 라이브러리, 프레임워크에서 만든 클래스를 등록할 때
@Component
주어진 클래스를 ‘컴포넌트’로 간주한다.
이 클래스들은 스프링 서버가 뜰 때 자동으로 감지된다.
@Component 덕분에 ****우리가 사용했던 어노테이션이 자동감지 되었다.
언제 @Component는 사용해야 할까?
컨트롤러, 서비스, 레포지토리가 모두 아니고
개발자가 직접 작성한 클래스를 스프링 빈으로 등록할 때 사용되기도 한다.
스프링 빈을 주입 받는 방법
(가장 권장) 생성자를 이용해 주입 받는 방식
setter와 @Autowired 사용 : 누군가 setter를 사용하면 오작동할 수 있음
필드에 직접 @Autowired 사용 : 테스트를 어렵게 만드는 요인
@Qualifier
스프링 빈을 사용하는 쪽, 스프링 빈을 등록하는 쪽 모두 @Qualifier를 사용할 수 있다.
스프링 빈을 사용하는 쪽에서만 쓰면, 빈의 이름을 적어주어야 한다.
@Primary vs @Qualifier
사용하는 쪽이 직접 적어준 @Qualifier가 이긴다.
DAY7 : Spring Data JPA를 사용한 데이터베이스 조작
23강. 문자열 SQL을 직접 사용하는 것이 너무 어렵다.
SQL을 직접 작성하면 아쉬운 점
문자열을 작성하기 때문에 실수할 수 있고, 실수를 인지하는 시점이 느리다.
컴파일 시점에 발견되지 않고, 런타임 시점에 발견된다.
특정 데이터베이스에 종속적이게 된다.
반복 작업이 많아진다. CRUD 쿼리가 항상 필요하다.
데이터베이스의 테이블과 객체는 패러다임이 다르다.
그래서 등장했다.
JPA (Java Persistence API) & 자바 진영의 ORM (Object-Relational Mapping)
Persistence (영속성) → 서버가 재시작되어도 데이터는 영구적으로 저장되는 속성
객체와 관계형 DB의 테이블을 짝지어 데이터를 영구적으로 보관하기 위해 Java 진영에서 정해진 규칙
HIBERNATE(구현체) — 구현(Implements) —> JPA
HIBERNATE은 내부적으로 JDBC를 사용한다.
24강. 유저 테이블에 대응되는 Entity Class 만들기
Java 객체와 MySQL Table을 매핑할 것이다.
User 객체 활용하여 실습진행
User Class에 @Entity 어노테이션을 붙인다.
@Entity : 스프링이 User 객체와 user 테이블을 같은 것으로 바라본다.
Entity : 저장되고, 관리되어야 하는 데이터
고유 id를 설정
@Id : 이 필드를 primary key로 간주
@GeneratedValue : primary key는 자동 생성되는 값
기본 생성자 생성
JPA를 사용하기 위해서는 기본 생성자가 꼭 필요하다.
// 예시 protected User() {}
@Column : 객체의 필드와 Table의 필드를 매핑한다.
여러 옵션이 존재한다.
// name varchar(20) @Column(nullable = false, length = 20, name = "name")
Column은 생략 가능하다. → 옵션이 필요없고 테이블과 동일하다면 생략이 가능하다.
JPA를 사용하니 추가적인 설정 필요 (한번만 하면 된다.)
application.yml
****jpa: hibernate: ddl-auto: none properties: hibernate: show_sql: true format_sql: true dialect: org.hibernate.dialect.MtSQL8Dialect
ddl-auto : 스프링이 시작할 때 DB에 있는 테이블을 어떻게 처리할지
create : 기존 테이블이 있다면 삭제 후 다시 생성
create-drop : 스프링이 종료될 때 테이블을 모두 제거
update : 객체와 테이블이 다른 부분만 변경
validate : 객체와 테이블이 동일한지 확인
none : 별다른 조치를 하지 않는다
show_sql : JPA를 사용해 DB에 SQL을 날릴 때 SQL을 보여줄 것인가
format_sql : SQL을 보여줄 때 예쁘게 포맷팅할 것인가
dialect : DB을 특정하면 조금씩 다른 SQL을 수정해준다.
25강. Spring Data JPA를 이용해 자동으로 쿼리 날리기
SQL을 작성하지 않고, 유저생성/조회/업데이트 기능을 리팩토링
UserRepository 인터페이스를 User 옆에 만들어준다.
JpaRepository를 상속 받는다.
package com.group.libraryapp.domain.user; import org.springframework.data.jpa.repository.JpaRepository; public interface UserRepository extends JpaRepository<User, Long> { }
저장기능 → save 메서드에 객체를 넣어주면 INSERT SQL이 자동으로 날아간다.
@Service public class UserServiceV2 { private final UserRepository userRepository; public UserServiceV2(UserRepository userRepository) { this.userRepository = userRepository; } public void saveUser(UserCreateRequest request) { // u에는 생성한 id가 반환된다. User u = userRepository.save(new User(request.getName(), request.getAge())); } }
조회기능 → findAll 메서드를 사용하면 모든 데이터를 가져온다.
// 자바8 문법을 이용한 public List<UserResponse> getUsers() { return userRepository.findAll() .stream().map(UserResponse :: new) .collect(Collectors.toList()); }
업데이트기능
Optional의 orElseThrow를 사용해 User가 없다면 예외를 던진다.
객체를 업데이트해주고 save 메서드를 이용하면 UPDATE 쿼리가 날아간다.
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); }
이렇게 동작할 수 있는 이유?
Spring Data JPA : 복잡한 JPA 코드를 스프링과 함께 쉽게 사용할 수 있도록 도와주는 라이브러리
우리가 사용한 메서드들은 SimpleJpaRepository에 담겨 있다.
26강. Spring Data JPA를 이용해 다양한 쿼리 작성하기
삭제 기능을 Spring Date JPA로 변경하자
public void deleteUser(String name) {
User user = userRepository.findByName(name);
if (user == null)
throw new IllegalArgumentException();
userRepository.delete(user);
}
findeByName 이란 SimpleJpaRepository에 존재하지 않는다. 그러므로 UserRepository에 findByName를 작성해주어야 한다.
public interface UserRepository extends
JpaRepository<User, Long> {
User findByName(String name);
}
반환 타입은 User, 유저가 없다면 null이 반환된다.
find → 1개 조회
By → SELECT 쿼리의 WHERE 문이 작성
By 앞에 들어갈 수 있는 구정 정리
find : 1개, 반환 타입은 객체 또는 Optional<타입> findAll : 쿼리의 결과물이 N개인 경우 사용. List<타입> 반환 exists : 쿼리 결과가 존재하는지 확인. 반환 타입은 boolean count : SQL의 결과 개수를 센다. 반환 타입은 long
각 구절은 And 또는 Or로 조합할 수 있다.
// List<user> findAllByNameAndAge(String name, int age);
SELECT * FROM user WHERE name = ??? AND age = ???;
By 뒤에 들어갈 수 있는 구절
GreaterThan : 초과
GreaterThanEqual : 이상
LessThan : 미만
LessThanEqual : 이하
Between : 사이에
StartsWith : ~로 시작하는
EndsWith : ~로 끝나는
By 뒤에 들어갈 수 있는 구절 정리
//List<User> findAllByAgeBetween(int startAge, int endAge);
SELECT * FROM user WHERE age BETWEEN ??? AND ???;
DAY8 : 트랜잭션과 영속성 컨테이너
27강. 트랜잭션 이론편
트랜잭션
쪼갤 수 없는 업무의 최소 단위
쇼핑몰에 주문을 한다면?
주문 기록 저장
포인트 기록 저장
주문 결제 기록 저장
만약 주문 기록과 포인트 기록이 저장되고 주문 결제 기록이 저장되지 않는다면 문제가 발생한다. 이러한 문제를 해결하기 위해 모두 성공시키거나 모두 실패시키자는 개념이 등장한다.
이렇게 쪼갤 수 없는 작업 단위를 트랜잭션이라고 한다.
트랜잭션 시작하기
start transaction;
정상 종료
commit;
실패 처리
rollback;
트랜잭션을 시작 후 commit이나 rollback를 하지 않으면 확인할 수 없다.
이는 묶여서 저장된다는 것을 의미한다.
28강. 트랜잭션 적용과 영속성 컨텍스트
@Transactional
어노테이션으로 붙이면 간단히 사용가능하다.
주의사항
IOException과 같은 Checked Exception은 롤백이 일어나지 않는다.
영속성 컨텍스트
테이블과 매핑된 Entity 객체를 관리/보관하는 역할
스프링에서는 트랜잭션을 사용하면 영속성 컨텍스트가 생겨나고, 트랜잭션이 종료되면 영속성 컨텍스트가 종료된다.
특수 능력 4가지
변경 감지 (Dirty Check)
영속성 컨텍스트 안에서 불러와진 Entity는 명시적으로 save하지 않더라도, 변경을 감지해 자동으로 저장된다.
@Transactional 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); }
save()를 명시적으로 하지 않아도 user.updateName()으로 user의 변경을 감지하고 save()를 하지 않아도 적용된다.
쓰기 지연
DB의 INSERT, UPDATE, DELETE SQL을 바로 날리는 것이 아니라, 트랜잭션이 commit될 때 모아서 한 번에 날린다.
1차 캐싱
ID를 기준으로 Entity를 기억한다.
이렇게 캐싱된 객체는 완전이 동일하다.
반복되는 findById(”1”)이 있다면 1번 할때, DB와 통신하여 id가 1인 무엇을 기억하고 다른 2번의 findById(”1”)은 통신하지 않고 조회할 수 있다.
DAY9 : 조금 더 복잡한 기능을 API로 구성하기
30강. 책 생성 API 개발하기
요구사항
책을 등록할 수 있다.
API 스펙 확인
HTTP Method : POST
HTTP Path : /book
HTTP Body (JSON)
{ "anme" : String }
결과 반환 X (HTTP 상태 200 OK이면 충분)
book 테이블 생성
create table book
(
id bigint auto_increment,
name varchar(255),
primary key (id)
);
@Column의 length 기본값은 255
문자열 필드는 최적화를 해야 하는 경우가 아닐 때 여유롭게 설정하는 것이 좋다.
book 객체를 만들어 설계했던 테이블과 맵핑 시키기
@Entity
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id = null;
@Column(nullable = false)
private String name;
}
BookService → loanBook() 생성
@Transactional
public void saveBook(BookCreateRequest request) {
bookRepository.save(new Book(request.getName()));
}
31강. 대출 기능 개발하기
요구사항
사용자가 책을 빌릴 수 있다. 다른 사람이 그책을 진작 빌렸다면, 빌릴 수 없다.
API 스펙 확인
HTTP Method : POST
HTTP Path : /book/loan
HTTP Body (JSON)
{ "userName" : String, "bookName" : String }
결과 반환 X (HTTP 상태 200 OK이면 충분)
user_loan_history 테이블 생성
create table user_loan_history
(
id bigint auto_increment,
user_id bigint,
book_name varchar(255),
is_return tinyint(1),
primary key (id)
);
UserLoanHistory 객체를 만들어 설계했던 테이블과 맵핑 시키기
public class UserLoanHistory {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id = null;
private long userId;
private String bookName;
private boolean isReturn;
}
boolean으로 처리하면 tnyint에 잘 매핑된다.
BookService → loanBook() 생성
@Transactional
public void loanBook(BookLoanRequest request) {
// 1. 책 정보를 가져온다.
Book book = bookRepository.findByName(request.getBookName())
.orElseThrow(IllegalAccessError::new);
// 2. 대출기록 정보를 확인해서 대출중인지 확인한다.
if (userLoanHitstoryRepository.existsByBookNameAndIsReturn(book.getName(), false)) {
// 3. 만약에 확인했는데 대출 중이라면 예외를 발생시킨다.
throw new IllegalArgumentException("진작 대출되어 있는 책입니다.");
};
// 4. 유저 정보를 가져온다.
User user = userRepository.findByName(request.getUserName())
.orElseThrow(IllegalAccessError::new);
// 5. 유저 정보와 책 정보를 기반으로 UserLoanHistory를 저장한다.
userLoanHitstoryRepository.save(new UserLoanHistory(user.getId(), book.getName()));
}
32강. 반납기능 개발하기
요구사항
유저가 책을 반납한다.
API 스펙 확인
HTTP Method : PUT
HTTP Path : /book/return
HTTP Body (JSON)
{ "userName" : String, "bookName" : String }
결과 반환 X (HTTP 상태 200 OK이면 충분)
이번 반납기능을 위한 API와 대출기능을 위한 API의 HTTP Body가 똑같다! 이런 경우 새로 만드는 것이 좋은지? 아니면 기존의 있는 DTO를 활용하는 것이 좋은지? 고민이 되는데 이러한 경우 새로 만드는 것을 추천한다고 한다.
이유 : 만약 두 기능 중 한 기능에 변화가 생겼을 때, 유연하고 side-effect 없이 대처할 수 있기 때문이다.
BookService → returnBook() 생성
@Transactional
public void returnBook(BookReturnRequest request) {
User user = userRepository.findByName(request.getUserName())
.orElseThrow(IllegalAccessError::new);
UserLoanHistory history = userLoanHitstoryRepository.findByUserIdAndBookName(user.getId(), request.getBookName())
.orElseThrow(IllegalAccessError::new);
history.doReturn();
}
@Transactional을 사용하고 있기 때문에 영속성 컨텍스트의 변경감지기능으로 엔티티객체의 변화를 감지하여 자동으로 업데이트된다!!
기능을 완성했지만 한 가지 고민할만한 내용이 존재한다.
우리가 ORM을 사용하게 된 이유 중 하나는 “DB 테이블과 객체는 패러다임이 다르기 때문”이다.
우리는 DISK(장기기억)와 RAM(메모리, 단기기억)의 차이 때문에 데이터의 영속성을 부여하기 위해서 DB테이블에 데이터를 저장하는 것은 필수이다.
하지만 JAVA 언어는 객체지향 언어이고, 대규모 웹 애플리케이션을 다룰 때에도 절차지향적 설계보다 객체지향적 설계가 좋다!
그러므로 우리는 좀 더 객체지향적으로 개발할 수 없을까?와 같은 고민을 할 필요가 있다.
DAY10 : 객체지향과 JPA 연관관계 - 33강
33강. 조금 더 객체지향적으로 개발할 수 없을까?
현재 대출기능과 반납기능을 보면 BookService에서 User와 UserLoanHistory를 모두 끌어다가 처리하는 것을 볼 수 있다. 이러한 부분을 수정하여 같은 도메인에 속해 있는 User와 UserLoanHistory가 협업하여 BookService에서 User만 가져와 대출 및 반납을 처리할 수 있게 수정한다.
선행조건 : User와 UserLoanHistory가 서로 알아야한다.
@Entity
public class UserLoanHistory {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id = null;
@ManyToOne
private User user;
private String bookName;
private boolean isReturn;
@ManyToOne : 내가 1이고 너가 Many이다. 즉, N:1 관계이다.
N:1 관계
학생과 교실을 생각하면 편하다. 학생(다수)와 교실(1)
반대로 User에는 @OneToMany를 붙여주어야 한다.
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id = null;
@Column(nullable = false, length = 20, name = "name") // name varchar(20)
private String name;
private Integer age;
@OneToMany(mappedBy = "user")
private List<UserLoanHistory> userLoanHistories = new ArrayList<>();
연관관계의 주인
테이블을 봤을 때, 누가 관계의 주도권을 가지고 있는가? 위 같은 경우 UserLoanHistory
주도권이라는 것은 누가 상대방을 도고 있는가로 생각하면 편하다.
@OneToMany(mappedBy = "user") 연관관계의 주인이 아닌 쪽에 mappedBy를 해주어야 한다.
2주차 과제
과제5
제시된 코드를 읽어보며, 더 좋은 코드로 고쳐보기
클린코드에 대해 배우고 또 알아보면서 생각보다 많은 항목을 주의할 필요가 있었다.
하지만 현재 그 많은 항목들을 주의하면서 코딩하기엔 부족한 점이 많은 것 같다는 생각이 들었다.
실제로 프로젝트를 하면서 느낀 점은 시간에 쫓겨 막 작성할 때가 있는데 특히 내 경우에는 중복 코드가 발생하고 네이밍을 할때 문제가 발생한다. 이번 과제를 통해 앞으로 주의하면서 코드를 작성하고자 한다.
댓글을 작성해보세요.