블로그

miiro

[인프런 워밍업 스터디 클럽 1기] BE 2주차 회고록

두 번째 발자국자바와 스프링 부트로 생애 최초 서버 만들기, 누구나 쉽게 개발부터 배포까지! [서버 개발 올인원 패키지]를 수강하고인프런 워밍업 클럽에 참여하여 쓰는 두 번째 회고록입니다.학습 내용static이 아닌 코드를 사용하려면 인스턴스화를 해야합니다.하지만, 학습에서 진행했던 UserController는 jdbcTemplate를 의존하여 사용하고 있습니다.이를 가능케 하는 것은 @RestController 어노테이션으로 의해 의존이 가능해지고 해당 클래스를 스프링 빈으로 등록시킵니다. 스프링 빈서버가 시작되면, 스프링 서버 내부에 거대한 컨테이너를 만듭니다.해당 컨테이너 안에는 클래스가 들어가게 되고, 다양한 정보(이름, 타입) 도 함께 들어가고, 인스턴스화도 이루어집니다.서버가 시작되면 코드의 구현 순서에 대해 살펴보겠습니다.스프링 컨테이너(즉, 클래스 저장소)가 시작됩니다.기본적으로 생성되어있는 스프링 빈들이 등록됩니다.사용자가 설정한 스프링 빈이 등록됩니다.필요한 의존성이 자동으로 설정이 됩니다. 여기서 스프링 컨테이너를 사용하는 이유를 살펴보면 2가지의 스프링 프레임워크의 특성 때문입니다.첫 번째는 제어의 역전(IOC) 이다. 말 그대로 메서드나 객체의 호출 작업을 개발자가 결정하는 것이 아닌, 외부(스프링 컨테이너)에서 결정되는 것을 의미입니다. 객체의 의존성을 역전시켜 객체간의 결합도를 줄이고 유연한 코드를 작성할 수 있게 해서 가독성 및 코드 중복, 유지 보수를 편하게 할 수 있도록 도와줍니다.두 번째는 의존성 주입(DI) 로 객체를 직접 생성하는 것이 아닌 외부에서 생성할 후 주입시키는 방식을 의미한다.이를 통해 모듈 간의 결합도가 낮아지고 유연성을 높일 수 있습니다.  스프링 빈으로 등록하는 방법여기서 중점적으로 사용하는 어노테이션에 대해 살펴보겠습니다. @Configuration클래스에 붙이는 어노테이션@Bean을 사용할 때 함께 사용외부 라이브러리, 프레임워크에서 만든 클래스를 등록할 때 사용한다.@Bean메서드에 붙이는 어노테이션메서드에서 반환되는 객체를 스프링 빈에 등록한다.외부 라이브러리, 프레임워크에서 만든 클래스를 등록할 때 사용한다.@Service, @Repository개발자가 직접 만든 클래스를 스프링 빈으로 등록할 때 사용한다.@Component주어진 클래스를 컴포넌트로 간주한다.해당 클래스들은 스프링 서버가 뜰 때 자동으로 감지된다. @Component 사용컨트롤러, 서비스, 레포지토리 X개발자가 직접 작성한 클래스를 스프링 빈으로 등록 시에 사용되기도 한다. 스프링 빈 주입 방법생성자를 활용하여 주입하는 방법setter와 @Autowired를 사용하는 방법필드에 직접 @Autowired를 사용하는 방법 JPA(Java Persistence API)데이터를 영구적으로 보관하기 위해 Java 진영에서 정해진 규칙영속성 : 서버가 재시작되어도 데이터는 영구적으로 저장되는 속성 ORM(Object-Relational Mapping)객체와 관계형 DB의 테이블을 짝짓는 방법HibernateJPA를 구현(implement)해서 코드로 작성한 구현체내부적으로 JDBC를 사용한다   JPA 어노테이션@Entity : 스프링이 객체와 테이블을 같은 것으로 바라본다.Entity 뜻 : 저장되고, 관리되어야 하는 데이터@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;@Id : 위 필드를 primary key로 간주@GeneratedValue : primary key는 자동 생성되는 값GenerationType.IDENTITY : MySQL의 auto_increment 전략과 매칭JPA를 사용하기 위해서는 기본생성자가 꼭 필요하다 @Column(nullable = false, length = 20, name = "name") private String name;@Column : 객체의 필드와 Table의 필드를 매칭한다.null 여부, 길이 제한, DB Column 이름 등을 사용@Column 어노테이션을 생략할 수도 있다.  트랜잭션(Transaction)쪼갤 수 없는 업무의 최소 단위 모든 SQL 한번에 성공 or 하나라도 실패하면 모두 실패 트랜잭션 명령어start transaction; : 트랜잭션 시작하기commit; : 트랜잭션 정상 종료(SQL 반영)rollback; : 트랜잭션 실패 처리(SQL 미반영)   트랜잭션 적용 방법@Transactional public void saveUser(UserCreateRequest request) { userRepository.save(new User(request.getName(), request.getAge())); }SELECT 쿼리만 사용한다면 readOnly 옵션 사용 가능@Transactional(readOnly = true) public List<UserResponse> getUsers() { return userRepository.findAll().stream() .map(UserResponse::new) .collect(Collectors.toList()); } ❗ 주의사항IOException 과 같은 Checked Exception은 롤백이 일어나지 않는다. 영속성 컨텍스트테이블과 매핑된 Entity 객체를 관리/보관하는 역할스프링에서는 트랜잭션을 사용하면 영속성 컨텍스트가 생겨나고, 트랜잭션이 종료되면 영속성 컨텍스트가 종료특징변경 감지(Dirty Check)영속성 컨텍스트 안에 불러와진 Entity는 명시적으로 save 하지 않더라도, 변경을 감지해 자동으로 저장된다.@Transactional public void updateUser(UserUpdateRequest request) { User user = userRepository.findById(request.getId()) .orElseThrow(IllegalArgumentException::new); user.updateName(user.getName()); }쓰기 지연DB의 insert/update/delete SQL문을 바로 날리는 것이 아닌, 트랜잭션이 commit될때 모아서 1번만 날린다.1차 캐싱ID를 기준으로 Entity를 기억한다.캐싱된 객체는 완전히 동일하다.(객체 주소도 같다) JPA 연관관계ex) 사람(person)과 실거주 주소(address)사람 1명은 1개의 실거주 주소만을 가지고 있다.@OneToOne연관관계의 주인을 설정한다.(mappedBy 사용한다.)연관관계의 주인 효과상대 테이블을 참조하고 있으면 연관관계의 주인이다.연관관계 주인이 아니면 mappedBy 사용연관관계의 주인의 setter가 사용되어야만 테이블이 연결@Transactional public void savePerson() { Person person = personRepotiory.save(new Person()); Address address = addressRepotiory.save(new Address()); person.setAddress(address); } 연관 관계의 주의점트랜잭션이 끝나지 않았기에, 한 쪽만 연결해두면 반대쪽의 값은 알 수 없다. @Transactional public void savePerson() { Person person = personRepotiory.save(new Person()); Address address = addressRepotiory.save(new Address ()); person.setAddress(address); System.out.println(address.getPerson); //null 반환 }이를 방지하기 위해 setter 한 번에 둘을 같이 이어준다.public void setAddress(Address address) { this.address = address; this.address.setPerson(this); } 다대일,일대다관계@ManyToOne을 단방향으로 사용할 수 있다.@JoinColumn연관관계의 주인이 활용할 수 있는 어노테이션필드의 이름이나, null 여부, 유일성 여부, 업데이트 여부 등을 지정할 수 있다.다대다관계(N : M 관계)@ManyToMany구조가 복잡하고, 테이블이 직관적으로 매핑되지 않기 때문에 사용하지 않는 것을 지양한다.cascade 옵션한 객체가 저장되거나 삭제될 때, 그 변경이 폭포처럼 흘러서 연결되어 있는 객체도 함께 저장되거나 삭제되는 기능orphanRemoval 옵션객체간의 관계가 끊어진 데이터를 자동으로 제거하는 옵션 @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) private List<UserLoanHistory> userLoanHistories = new ArrayList<>();지연 로딩(Lazy Loading)연결되어 있는 객체를 꼭 필요한 순간에 데이터를 로딩한다. @OneToMany의 fetch() 옵션 연관관계의 장점각자의 역할에 집중하게 된다.새로운 개발자가 코드를 읽을 때 이해하기 쉬워진다.테스트 코드 작성이 쉬워진다.연관관계 사용 시 주의 사항지나치게 사용하면 성능 상의 문제가 발생할 수 있다.도메인 간의 복잡한 연결로 인해 시스템을 파악하기 어려워질 수 있다.요구 사항 등 여러 부분을 고민해서 연관 관계 사용을 선택해야한다. 과제 내용Day 4Day2에서 진행했던 GET API와 POST API를 구현했었는데, 그 당시에는 database를 사용하지 않은 방법으로 과제에서 주어진 조건에 맞게 답을 출력했었습니다. 이번 과제에서는 DB를 활용하여 정보를 저장하고, 수정할 수 있는 로직을 구현해야했습니다.Layer를 분리해서 진행을 할 수 있지만, 간단하게 문제의 답만 도출하기 위해서 Controller Class에 구현을 진행했었습니다.그렇기 때문에 Spring Data JPA가 아닌 JDBCTemplate를 활용하여 진행했습니다. JPA를 통한 DB 로직을 구현하는 것만 주구장창 했어서, 직접 쿼리문을 작성해서 진행하는 JDBCTemplate를 사용하는 방법에 대해 알 수 있었던 거 같습니다.📋 4일차 미션 : GET API와 POST API 구현 Day 5클린 코드는 코드를 아름답게 만드는 것이 아닌 코드의 질을 향상 시키는 데 중요하다는 것을 알았습니다.클린 코드를 구현하기 위해서 명확하고 간결하게 코드를 구현하여 버그를 줄이고, 그로 인한 개발 속도를 향상시킬 수 있습니다.또한, 잘 작성된 코드는 시간이 흘러도 이해하기 쉽기에, 유지 보수가 간편합니다. 이로 인해 개발자로서 팀 프로젝트를 진행할 시에팀 커뮤니케이션을 원활하게 진행할 수 있도록 도움을 줄 수 있다는 것을 알게 되었습니다.📋 5일차 미션 : 클린 코드 구현  회고스프링 프레임워크의 핵심 개념인 제어의 역전(IOC)과 의존성 주입(DI)을 통해 더 나은 코드 작성과 유지 보수의 용이섬에 대해 알게 되었습니다. 또한 어노테이션들을 활용하여 클래스를 스프링 빈으로 등록하고, 스프링 컨테이너가 이를 관리함으로써, 필요한 의존성을 자동으로 설정할 수 있다는 사실을 배웠습니다. JPA를 통한 데이터의 영속성 관리와 ORM을 통한 객체와 DB의 매핑에 대해 이론적으로 정립할 수 있는 계기되었던 거 같습니다.서비스를 구성하는 데 가장 중요한 @Transactional 어노테이션을 사용하여 데이터의 일관성을 유지하는 방법을 쉽게 공부하면서 더티 체크, 쓰기 지연 등 특성에 대해 알 수 있었던 거 같습니다.객체와 DB를 효과적으로 설계할 수 있도록 연관관계를 설계하는 방법을 배움으로써 사용할 때의 주의사항과 성능 문제를 방지하기 위한 전략에 대해 알게 되면서 이를 통해 효과적이고 용이한 애플리케이션 개발을 할 수 있는 토대를 마련할 수 있었던 거 같습니다.  

백엔드인프런워밍업스터디클럽2주차회고록

이혜리

[인프런 워밍업 클럽 스터디1기] 백엔드 - 2주차 회고

2주차 정리 및 회고Section3. 역할의 분리와 스프링 컨테이너클린코드클린코드 : 함수는 최대한 작게 만들고 한 가지 일만 수행하는 것이 좋다. UserController.java 가api 진입점,현재 유저가 있는지 없는지 확인하고 예외처리,SQL을 사용해 실제 database와의 통신  을 담당하는 3가지 역할을 다 하고 있으므로, 클린코드가 아니다. 이러한 상태의 단점은 너무 큰 기능이기 때문에 테스트도 힘들다.종합적으로 유지보수성이 매우 떨어진다.따라서 Controller를 3단 분리하여 클린 코드로 작성하였다. 기존 api 진입점으로써 HTTP Body를 객체로 변환의 역할은 Controller 의 역할로 남겨두고,현재 유저가 있는지 없는지 확인하고 예외처리 의 역할은 Service가,SQL을 사용해 실제 database와의 통신 의 역할은 Repository가 담당한다. 스프링 컨테이너서버가 시작되면, 스프링 컨테이너(클래스 저장소)가 시작된다. 스프링 빈들(클래스들) 이 등록되고 - dependency 주입된, 사용자가 직접 설정해준 스프링 빈이 등록된다. 이때 필요한 의존성이 자동으로 설정된다.예를 들어, UserController에서 필요한 JdbcTemplate이 자동으로 생성자 내로 들어간다. 위와 같이 Book 관련 3단 분리 코드를 예시로 봤다.이때, 두 repository 중 어떤 것을 우선순위로 하는지는 @Primary @Qualifier 어노테이션을 사용하면 된다. Section4. 생애 최초 JPA 사용하기JPA 사용지금까지 작성한 코드를 살펴보면 아쉬운 몇 가지가 있다.repository 클래스 내에서 문자열로 쿼리를 작성하기 때문에 실수할 수 있고, 실수를 인지하는 시점이 느리다.특정 데이터베이스에 종속적이게 된다. 우리의 경우엔 MySql반복 작업이 많아진다.데이터베이스의 테이블과 객체의 매핑되는 패러다임이 다르다.따라서 JPA (Java Persistence API) 데이터를 영구적으로 보관하기 위해 java 진영에서 정해진 규칙을 사용한다.즉, 객체와 관계형 데이터베이스 테이블을 짝지어 데이터를 영구적으로 저장할 수 있도록 돕는다.이를 사용하기 위해서는 Hibernate 가 필요하다.직접 매핑해보자유저 테이블에 대응되는 entity class 인 User.java 를 만들면 다음과 같이 코드가 수정된다.package com.group.libraryapp.domain; import org.springframework.lang.Nullable; import javax.persistence.*; @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; @Column(nullable = false) private Integer age; protected User() { } public User(String name, Integer age) { if (name == null || name.isBlank()){ throw new IllegalArgumentException(String.format("잘못된 name(%s)이 들어왔습니다.")); } this.name = name; this.age = age; } public String getName() { return name; } public Integer getAge() { return age; } public Long getId() { return id; } public void updateName(String name) { this.name = name; } } 또한 jpa 를 사용하기 위해서 application.yml 파일도 변경하자.hibernate 부분을 추가해준다.spring: datasource: url: "jdbc:mysql://localhost/library" username: "root" password: "1234" 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😀 spring.jpa.hibrenate.ddl-auto : none스프링이 시작할 때 DB에 있는 테이블을 어떻게 처리할지에 대한 옵션이다. 현재 DB에 테이블이 잘 만들어져 있고, 미리 넣어둔 데이터도 있으므로 별 다른 조치를 하지 않는다. 자동으로 쿼리 날리기 (기본)repository 폴더 내에 UserRepository interface를 만들어준 뒤,JpaRepository 를 상속받게 해준다.package com.group.libraryapp.repository.user; import com.group.libraryapp.domain.User; import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; public interface UserRepository extends JpaRepository<User, Long> { Optional<User> findByName(String name); }UserService.java에서save, findAll, delete 등의 기본적인 쿼리들은 sql 문자열을 타이핑할 필요없이 자동으로날릴 수 있게 되었다.package com.group.libraryapp.service.user; import com.group.libraryapp.domain.User; import com.group.libraryapp.repository.user.UserRepository; import com.group.libraryapp.dto.User.request.UserCreateRequest; import com.group.libraryapp.dto.User.request.UserUpdateRequest; import com.group.libraryapp.dto.User.response.UserResponse; import org.springframework.stereotype.Service; import java.util.List; import java.util.stream.Collectors; @Service public class UserServiceV2 { private final UserRepository userRepository; public UserServiceV2(UserRepository userRepository) { this.userRepository = userRepository; } public void saveUser(UserCreateRequest request){ userRepository.save(new User(request.getName(),request.getAge())); }// INSERT SQL 이 자동으로 날라감. public List<UserResponse> getUser(){ return userRepository.findAll().stream() .map(UserResponse::new ) .collect(Collectors.toList()); } public void updateUser(UserUpdateRequest request){ User user = userRepository.findById(request.getId()) .orElseThrow(IllegalArgumentException::new); user.updateName(request.getName()); userRepository.save(user); } public void deleteUser(String name){ User user = userRepository.findByName(name) .orElseThrow(IllegalArgumentException::new); userRepository.delete(user); } }JpaRepository<Entity, ID>를 구현 받는 Repository에 대해 자동으로 기본적인 메소드(save, findAll) 를 사용할 수 있는 SimpleJpaRepository 기능을 사용할 수 있게 해준다. 그렇다면 다른 다양한 쿼리는 어떻게 작성할까?위 코드에서 삭제 기능인 deleteUser 메소드는userRepository.findByName 메소드를 사용하고 있다.이 메소드는 SimpleJpaRepository 기능이 아니라, 추가로 interface에 추상(?) 함수 정의만 해놓은 findByName 을 사용한 것이다. By 앞에는 다음과 같은 구절이 들어갈 수 있다. findfindAllexistscountBy 뒤에는 필드 이름이 들어가는데, And나 Or 로 조합될 수 있다. 또한 동등조건 (=) 외에도 다양한 조건을 사용할 수 있다.회고벌써 워밍업 클럽 스터디의 반이나 지났다. 확실히 이렇게 약간의 강제성을 부여하는 스터디가 나랑 잘 맞는 것 같다. 나 혼자서 공부했으면, 금방 흐지부지 되었을텐데 스터디 코치님들이 디스코드에 종종 올려주시는 글들이나, 다른 분들이 과제한 부분들을 읽으면서 배우는 점도 많은 것 같고, 완주러너의 혜택도 욕심이 나서 더욱 공부를 하게 되는 것 같다.

백엔드백엔드3단분리JPA2주차발자국

나무님

인프런 워밍업 클럽 - BE 0기, 회고 #2

인프런 워밍업 클럽 2주 차 학습이 종료되었다. 강의 수강지난주 강의까지는 비교적 수월하게 따라갈 수 있었지만 이번 주에는 JPA를 배우면서 강의를 따라가는 데 약간의 어려움이 있었다.강의를 들을 때는 이해를 하고 넘어간 것 같은데 과제를 하면서 직접 코딩을 시작하니 내가 짠 코드가 맞는지 의문이 생겼다.특히 쿼리를 사용하지 않고 Spring Data JPA 쿼리 네이밍 룰에 맞게 이름을 작성하는 게 어려웠다.검색과 자동완성을 통해 네이밍 룰을 작성하다 보니 조금 감을 잡긴 했지만, 아직 익숙하지 않고 원하는 결과를 얻으려면 어떤 네이밍 룰을 맞춰줘야 하는지 알 수 없어서 더 오래 걸린 것 같다.익숙해지면 훨씬 쉽게 사용할 수 있을 것 같다.Entity에서 각 객체끼리 연관관계를 맺는 것도 아직은 조금 어려운 것 같다.개념은 이해했는데 어느 테이블이 N이고 어느 테이블이 1인지 파악하는 것도 헷갈리고, 매핑을 해서 데이터를 가져오는 것도 헷갈린다.이제까지 사용해보지 않은 JPA를 배우면서 처음에는 많이 헤맸지만 강의를 다시 한번 보고 아직 이해되지 않는 부분을 해결하기 위해 다른 자료를 찾아보며 지식을 채워 넣으니 약간은 뿌듯한 마음이 들었다.한 단계 더 성장한 느낌이 든다.과제6일차 과제7일차 과제미니프로젝트(진행중)  이번 주에 느낀점.1. Spring Data JPA 쿼리 네이밍 룰 공부2.연관관계 매핑 공부 다음주도 화이팅!!!🙌

백엔드인프런워밍업클럽인프런워밍업클럽BE0기회고2주차