블로그
전체 8#카테고리
- 백엔드
#태그
- 백엔드
- aws
- 배포
- jpa연관관계
- 7차
- 과제
- JPA
- Entity
- 테스트코드
- 4차과제
- mock
- 3단분리
- 2주차
- 발자국
- 워밍업
- 1기
- 6차
- 1주차
- 회고
- 3차
- 인프런
- 스터디
2024. 05. 19.
0
[인프런 워밍업 클럽 1기/BE] 3번째 발자국
section531강. 대출 기능 개발하기32강. 책 반납 기능 개발하기33강. 조금 더 객체지향적으로 개발할 수 없을까?34강. JPA 연관관계에 대한 추가적인 기능들35강. 책 대출/반납 기능 리팩토링과 지연 로딩1. 대출기능 개발 - 새로운 테이블 생성현재 user, book 2개의 테이블이 존재한다. 하지만 이 2개의 테이블 만으로는 대출 기능을 만들 수 없다. 새로운 테이블 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) )user_id : 어떤 유저가 빌렸는지 알 수 있도록, 유저의 id를 가지고 있도록 한다.is_return : 타입은 tinyint 인데, entity 객체의 필드 중 boolean에 매핑하게 되면, true인 경우 1, false인 경우 0이 저장된다.2. 책 반납 기능 개발 - @ManyToOne 으로 리팩토링 위의 HTTP Body 는 반납 request 의 요청 형식이다. 그런데 '책 대출' 과 '책 반납'의 api body가 똑같다. 이때 똑같더라도 별개의 class로 작성하는 것이 좋다. 두 기능 중 한 기능에 변화가 생겼을때, 유연하고 다른 부가적인 문제없이 대처할 수 있기 때문이다.아래는 반납 관련 DTO와 Controller, service 내용이다.DTOpublic class BookReturnRequest { private String userName; private String bookName; public BookReturnRequest(String userName, String bookName) { this.userName = userName; this.bookName = bookName; } public String getUserName() { return userName; } public String getBookName() { return bookName; } }Controller @PutMapping("/book/return") public void returnBook(@RequestBody BookReturnRequest request){ bookService.returnBook(request); }Service@Transactional public void returnBook(BookReturnRequest request) { User user = userRepository.findByName(request.getUserName()) .orElseThrow(IllegalArgumentException::new); UserLoanHistory history = userLoanHistoryRepository.findByUserIdAndBookName(user.get Id(), request.getBookName()) .orElseThrow(IllegalArgumentException::new); history.doReturn(); }위의 코드를 조금 더 객체지향적으로 개발하기 위해서JPA 연관관계를 활용할 수 있다. 이렇게 바꾸기 위해서는 UserLoanHistory와 User 가 서로 직접 알고 있어야 한다.UserLoanHistory의 userId 를 user로 변경해보자. @Entity public class UserLoanHistory { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id = null; @JoinColumn(nullable = false) @ManyToOne private User user; private String bookName; private boolean isReturn; public Long getId() { return id; } public String getBookName() { return bookName; } public boolean isReturn() { return isReturn; } public UserLoanHistory(User user, String bookName) { this.user = user; this.bookName = bookName; this.isReturn = false; } public void doReturn(){ this.isReturn = true; } public UserLoanHistory() { } }@ManyToOne 은 N(나) : 1(너) 관계로 위에서는 N이 UserLoanHistory가 되고, 1이 User가 된다.User 클래스에서 1명의 유저는 N개의 UserLoanHistroy를 가지고 있을 수 있기 때문에, UserLoanHistroy를 List 형태로 가지고 있어야 한다.@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; @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) //주인이 가진 필드 이름 // fetch = FetchType.LAZY private List userLoanHistories = new ArrayList(); protected User() { } public Long getId() { return id; } public String getName() { return name; } public int getAge() { return age; } public void updateName(String name){ this.name = name; } public User(String name, int age) { if (name == null || name.isEmpty()) throw new IllegalArgumentException(String.format("잘못된 name(%s)이 들어왔습니다.", name)); this.name = name; this.age = age; } public void loanBook(String bookName){ this.userLoanHistories.add(new UserLoanHistory(this, bookName)); } public void returnBook(String bookName){ UserLoanHistory targetHistroy = this.userLoanHistories.stream() .filter(history -> history.getBookName().equals(bookName)) .findFirst() .orElseThrow(IllegalArgumentException::new); targetHistroy.doReturn(); } }요 List에는 @OneToMany 를 붙여준다.이때 연관관계의 주인을 정해주어야 한다. 현재 user 테이블과 user_loan_history 테이블을 보면, user_loan_history는 user를 알고 있다. 반면 user는 user_loan_history 를 알지 못한다. 즉, 관계의 주도권을 user_loan_history 가 가지고 있는 것이다.테이블에서는 이를 알 수 있지만,JPA 에서는 모르는 상태이니 mappedBy 옵션을 달아주어 이제 알려주자.user가 주인이 아니므로, @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) //주인이 가진 필드 이름 // fetch = FetchType.LAZY private List userLoanHistories = new ArrayList();이렇게 하여 user 와 userLoanHistory가 서로를 알 수 있도록 하였다. 하지만, 여전히 BookService는 User와 UserLoanHistory를 각자 다루고 있다. 온전히 협력하지 못하므로 이를 수정해보자.+ @JoinColumn 은 연관관계의 주인 클래스에서 사용할 수 있다. Service 코드에서 UserLoanHistory를 직접 사용하지 않고, User 를 통해 대출 기록을 저장하도록 변경해보자.일단 BookService는 아래와 같이 변경했다. @Transactional public void loanBook(BookLoanRequest request) { //1. 책 정보를 가져온다. Book book = bookRepository.findByName(request.getBookName()) .orElseThrow(IllegalArgumentException::new); //2. 대출기록 정보를 확인해서 대출중인지 확인합니다. //3. 먄약에 확인했는데 대출중이라면 예외를 발생시킨다. if (userLoanHistoryRepository.existsByBookNameAndIsReturn(book.getName(), false)) //대여중임 throw new IllegalArgumentException("이미 대출되어 있는 책입니다."); //4. 유저 정보를 가져온다. User user = userRepository.findByName(request.getUserName()) .orElseThrow(IllegalArgumentException::new); user.loanBook(book.getName()); }위에서 UserLoanHistory 객체를 직접적으로 사용하지 않고 있다. user 객체의 함수인 loanBook를 불러오고 있는데 여기 메소드를 살펴보면, public void loanBook(String bookName){ this.userLoanHistories.add(new UserLoanHistory(this, bookName));User 의 필드중 userLoanHistories 에 UserLoanHistory 객체를 집어넣는다.이렇게 바꾸어서 User 와 UserLoanHistory 2개 객체가 서로 협력하도록 변경했다.section 6배포를 하기 위해서는 aws 의 ec2를 사용한다.ec2는 계속 돌아가는 전용 컴퓨터와 비슷한 개념이다. ec2 인스턴스를 생성한 후, 이에 ssh 연결하여 필요한 것들을 설치한 후, 프로젝트 build 후 실행을 background에서 하면 된다.회고강의를 90% 들은 시점에서 들은 생각은 이 강의와 인프런 워밍업 클럽 스터디를 하기 잘했다는 것이다. 이제는 기본적으로 api를 보낼 수 있으니, 앞으로는 시큐리티 부분과 테스트 코드 위주로 공부를 더 해나가려 한다.
백엔드
・
백엔드
・
aws
・
배포
・
jpa연관관계
2024. 05. 16.
0
[인프런 워밍업 클럽 스터디1기] 백엔드 - 7차 과제
진도표 7일차와 연결됩니다우리는 JPA라는 개념을 배우고 유저 테이블에 JPA를 적용해 보았습니다. 몇 가지 문제를 통해 JPA를 연습해 봅시다! 🔥 문제1JPA(Java Persistence API) 는 자바 객체를 관계형 데이터 베이스에 영속적으로 저장하고 조회할 수 있는 ORM 기술에 대한 표준 명세를 의미한다.JPA 를 통해 SQL 쿼리를 작성하지 않고도 객체를 통해 데이터베이스를 조작할 수 있어 보수성이 향상된다.Entity 클래스를 작성한 후 Repository 인터페이스를 생성해야하는데, JpaRepository를 상속받도록 하면, 기본적인 쿼리 추가, 조회, 수정, 삭제, findAll(), findById() 등의 메서드를 사용할 수 있다.1) Entity 클래스 정의@Entity public class Fruits { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id = null; @Column(nullable = false, length = 20 ) private String name; @Column(nullable = false) private LocalDate warehousingDate; @Column(nullable = false) private long price; @Column(nullable = false) private int saled; public Fruits(){ } public Fruits(long id, String name, LocalDate warehousingDate, long price, int saled) { this.id = id; this.name = name; this.warehousingDate = warehousingDate; this.price = price; this.saled = saled; } public Fruits(String name, LocalDate warehousingDate, long price, int saled) { this.name = name; this.warehousingDate = warehousingDate; this.price = price; this.saled = saled; } public Long getId() { return id; } public String getName() { return name; } public LocalDate getWarehousingDate() { return warehousingDate; } public long getPrice() { return price; } public int getSaled(){ return saled; } public void setSaled(int saled) { this.saled = saled; } }JPARepository를 사용하여 액세스할 엔티티 클래스를 정의했다.@Entity 어노테이션은 JPA를 사용해 테이블과 매핑할 클래스에 붙여주는 어노테이션이다. 이 어노테이션을 붙이면, JPA가 해당 클래스를 관리하게 된다.@Id 어노테이션은 특정 속성을 기본키로 설정하는 어노테이션이다.@GeneratedValue(startegy = GenerationType.IDENTITY) 은 기본키 생성을 DB에 위임한다는 것으로 위 클래스에서는 id 값이 자동 생성된다.@Column 은 객체필드를 테이블 칼럼과 매핑한다. 2) JpaRepository 인터페이스 상속받는 인터페이스 생성public interface FruitRepository extends JpaRepository { Optional findById(Long id); long countByNameAndSaled(String name,int i); long countByName(String name); List findByPriceGreaterThan(long price); List findByPriceLessThan(long price); List findBySaled(int i); } 문제2특정 과일을 기준으로 지금까지 우리 가게를 거쳐갔던 과일 개수를 세고 싶습니다.controller 클래스에서는 아래와 같이 getmapping 부분을 만들고@GetMapping("/api/v1/fruit/count") @Description("지금까지 거쳐간 특정 과일이름의 개수를 반환(7일차-문제2)") public FruitCountResponse countName(@RequestParam String name){ return fruitService.countName(name); }service 클래스에서는 public FruitCountResponse countName(String name) { Long result = fruitRepository.countByName(name); return new FruitCountResponse(result); }위와 같이 FruitCountResponse 객체를 반환하도록 한다.FruitCountResponse 클래스는 아래와 같이 간단하게 json 형식으로 "count" : 숫자 를 반환하도록 만들었다.public class FruitCountResponse { private long count; public FruitCountResponse(long count) { this.count = count; } public long getCount() { return count; } }아래와 같이 HTTP 응답 Body 예시와 같은 형식으로 나오는 것을 볼 수 있다. 문제3 아직 판매되지 않은 특정 금액 이상 혹은 특정 금액 이하의 과일 목록을 받아보고 싶습니다.controller 클래스에서는 @GetMapping("/api/v1/fruit/list") @Description("판매되지 않은 특정 금액 이상 혹은 특정 금액 이하의 과일 목록(7일차-문제3)") public List getListFruit(@RequestParam String option, @RequestParam long price){ return fruitService.overviewFruitCondition(option,price); }service 클래스에서는public List overviewFruitCondition(String option, long price) { List list; List result = new ArrayList(); if (option.equals("GTE")) list = fruitRepository.findByPriceGreaterThan(price); else if (option.equals("LTE")){ list = fruitRepository.findByPriceLessThan(price); }else throw new IllegalArgumentException("GTE, LTE 로 작성해주세요."); for (Fruits e : list) if (e.getSaled() == 1) result.add(e); return result; }위와 같이 List 를 반환하도록 한다. 위와 같이 조건을 만족하는 (6000 이하의 과일들) 결과를 출력한다. 회고직접 JpaRepository를 상속받는 repository와 entity 를 만들어 실습해보니, 약간 익숙해진 것 같다. 위 과제를 하는 중간중간 오류(1,2) 가 났었는데,1) getter of property ‘id’ threw exceptionhttps://stackoverflow.com/questions/25234892/org-springframework-beans-invalidpropertyexception-invalid-property-id-of-bea org.springframework.beans.InvalidPropertyException: Invalid property 'id' of bean classI dont understand why I am getting this error on the save below. Any clue? org.springframework.beans.InvalidPropertyException: Invalid property 'id' of bean class [com.test.DataException]: Getter...stackoverflow.com위 오류에서는 long에서 Long으로 id 타입을 바꾸니 해결되었다.2) Unknown column 'warehousing_date' in 'field list'위 오류에서는 warehousing_date 라는 칼럼명을 못 찾아서 데이터를 save 할 수 없는 상황이였는데,변수 값을 잘못 넣어서 오류가 났던 거라 바꾸니 해결되었다.
백엔드
・
백엔드
・
7차
・
과제
・
JPA
・
Entity
2024. 05. 12.
0
[인프런 워밍업 클럽 스터디1기] 백엔드 - 테스트코드(feat.4차과제)
과제4는 과일 가게에 입고된 과일에 대한 API 를 만드는 과제이다.이 과제로 테스트코드를 작성해보았다.새로운 과일을 등록하는 API (post) 를 테스트하는 부분과이미 등록된 과일을 팔아 saled 필드를 0으로 바꾸는 API (put) 을 테스트하는 부분으로 나누어 2개를 작성하였다.FruitControllerTest.javapackage com.group.libraryapp.controllerTest; import com.fasterxml.jackson.databind.ObjectMapper; import com.group.libraryapp.dto.fruit.FruitCreateRequest; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; import java.time.LocalDate; import static org.springframework.test.web.client.match.MockRestRequestMatchers.content; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view; @ExtendWith(SpringExtension.class) @SpringBootTest @AutoConfigureMockMvc public class FruitControllerTest { @Autowired MockMvc mockMvc; @Autowired ObjectMapper objectMapper; @Test @DisplayName("과일 새로 등록 테스트(성공)") public void saveFruitTest() throws Exception{ //given String name = "자두"; LocalDate date = LocalDate.parse("2024-05-12"); long price = 3000; FruitCreateRequest fruitCreateRequest = new FruitCreateRequest(name,date,price); String url = "http://localhost:8080" + "/api/v1/fruit"; //when final ResultActions resultActions = mockMvc.perform(post(url) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(fruitCreateRequest)) ).andDo(print()); //then resultActions .andExpect(status().isOk()); } @Test @DisplayName("과일 판매후 등록 테스트(성공)") public void saledFruit() throws Exception{ //given String url = "http://localhost:8080" + "/api/v1/fruit"; //when final ResultActions resultActions = mockMvc.perform(put(url) .queryParam("id","9") ).andDo(print()); //then resultActions .andExpect(status().isOk()); } }테스트코드에서는 실제 객체와 비슷하지만 테스트에 필요한 기능만 가지는 가짜 객체를 만들어서 애플리케이션 서버에 배포하지 않고도 스프링 MVC 동작을 재현할 수 있는 클래스를 사용한다.어지저찌 작성해서 테스트는 통과했다.테스트가 실패하는 경우와 나머지 api에 대해서도 추후 추가할 예정이다.아래 블로그들 내용을 참고해서 작성해보았는데아직 모르는게 너무 많은 것 같다.. https://shinsunyoung.tistory.com/52https://velog.io/@minseokangq/REST-API-CRUD
백엔드
・
테스트코드
・
4차과제
・
백엔드
・
mock
2024. 05. 12.
0
[인프런 워밍업 클럽 스터디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 { Optional 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 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를 구현 받는 Repository에 대해 자동으로 기본적인 메소드(save, findAll) 를 사용할 수 있는 SimpleJpaRepository 기능을 사용할 수 있게 해준다. 그렇다면 다른 다양한 쿼리는 어떻게 작성할까?위 코드에서 삭제 기능인 deleteUser 메소드는userRepository.findByName 메소드를 사용하고 있다.이 메소드는 SimpleJpaRepository 기능이 아니라, 추가로 interface에 추상(?) 함수 정의만 해놓은 findByName 을 사용한 것이다. By 앞에는 다음과 같은 구절이 들어갈 수 있다. findfindAllexistscountBy 뒤에는 필드 이름이 들어가는데, And나 Or 로 조합될 수 있다. 또한 동등조건 (=) 외에도 다양한 조건을 사용할 수 있다.회고벌써 워밍업 클럽 스터디의 반이나 지났다. 확실히 이렇게 약간의 강제성을 부여하는 스터디가 나랑 잘 맞는 것 같다. 나 혼자서 공부했으면, 금방 흐지부지 되었을텐데 스터디 코치님들이 디스코드에 종종 올려주시는 글들이나, 다른 분들이 과제한 부분들을 읽으면서 배우는 점도 많은 것 같고, 완주러너의 혜택도 욕심이 나서 더욱 공부를 하게 되는 것 같다.
백엔드
・
백엔드
・
3단분리
・
JPA
・
2주차
・
발자국
2024. 05. 11.
0
[인프런 워밍업 클럽 스터디1기] 백엔드 - 6차 과제
진도표 6일차와 연결됩니다우리는 스프링 컨테이너의 개념을 배우고, 기존에 작성했던 Controller 코드를 3단 분리해보았습니다. 앞으로 API를 개발할 때는 이 계층에 맞게 각 코드가 작성되어야 합니다! 🙂과제 #4 에서 만들었던 API를 분리해보며, Controller - Service - Repository 계층에 익숙해져 봅시다! 👍 controller, service, repository로 분리해보자. 파일의 폴더 구조는 아래와 같다. controller, domain, dto, repository, service 폴더로 이루어져 있으며, 각각에 해당하는 파일들이 위치한다. FruitController.javapackage com.group.libraryapp.controller.fruit; import com.group.libraryapp.domain.Fruit; import com.group.libraryapp.dto.fruit.FruitCreateRequest; import com.group.libraryapp.dto.fruit.FruitOverviewResponse; import com.group.libraryapp.service.FruitService; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.web.bind.annotation.*; import java.time.LocalDate; import java.util.ArrayList; import java.util.List; @RestController public class FruitController { private final FruitService fruitService; public FruitController(FruitService fruitService) { this.fruitService = fruitService; } //문제1 @PostMapping("/api/v1/fruit") public void saveFruit(@RequestBody FruitCreateRequest request) { fruitService.saveFruit(request); } //문제2 @PutMapping("/api/v1/fruit") public void saledFruit(@RequestParam int id){ fruitService.saleFruit(id); } //문제3 @GetMapping("/api/v1/fruit/stat") public FruitOverviewResponse overviewFruit(@RequestParam String name){ return fruitService.overviewFruit(name); } } controller 파일은 심플하게 service의 메소드를 불러오는 방식으로 리팩토링했다.즉, 메소드만 명시하고, 실제적인 일은 service, controller가 한다는 뜻!위와 같이 코드를 짬으로서, api 의 진입점의 역할을 잘 하고 있다 볼 수 있다.FruitService.javapackage com.group.libraryapp.service; import com.group.libraryapp.domain.Fruit; import com.group.libraryapp.dto.fruit.FruitCreateRequest; import com.group.libraryapp.dto.fruit.FruitOverviewResponse; import com.group.libraryapp.repository.FruitRepository; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; import java.util.List; @Service public class FruitService { private final FruitRepository fruitRepository; private FruitOverviewResponse fruitOverviewResponse; public FruitService(@Qualifier("sql") FruitRepository fruitRepository, FruitOverviewResponse fruitOverviewResponse) { this.fruitRepository = fruitRepository; this.fruitOverviewResponse = fruitOverviewResponse; } public void saveFruit(FruitCreateRequest request){ fruitRepository.saveFruit(request.getName(),request.getPrice(),request.getWarehousingDate()); } public void saleFruit(int id) { fruitRepository.saleFruit(id); } public FruitOverviewResponse overviewFruit(String name) { List list = fruitRepository.overviewFruit(name); long notsalesamount = 0; long salesamount = 0; for(Fruit e : list){ if (e.getSaled() == 1) notsalesamount += e.getPrice(); else salesamount += e.getPrice(); } fruitOverviewResponse.setNotSalesAmount(notsalesamount); fruitOverviewResponse.setSalesAmount(salesamount); return fruitOverviewResponse; } }FruitService는 FruitRepository의 메소드를 불러오는 역할과 동시에 유저가 있는지 없는지를 확인하고 예외처리를 하는 부분이다.위 클래스의 overviewFruit 함수에서선별한 정보를 아래와 같은 HTTP 응답 Body로 반환하기 위한 처리를 하도록 수정하였다. list 형태의 반환값을 FruitRepository의 메소드로 부터 받은 후, 이를 FruitOverviewResponse 객체로 반환하는 역할을 하고 있다. FruitRepository.javapackage com.group.libraryapp.repository; import com.group.libraryapp.domain.Fruit; import java.time.LocalDate; import java.util.List; public interface FruitRepository { public void saveFruit(String name, long price, LocalDate warehousingDate); public void saleFruit(int id); public List overviewFruit(String name); } 문제2의 요구사항을 위해, repository를 바꿔가며 동작시킬 수 있도록 강의에서 이 경우에 interface를 작성하였기 때문에 interface 클래스를 작성하였다. @Primary 어노테이션 대신, @Qualifer 어노테이션을 FruitService 클래스에 사용했다.FruitMemoryRepository.javapackage com.group.libraryapp.repository; import com.group.libraryapp.domain.Fruit; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; import java.time.LocalDate; import java.util.ArrayList; import java.util.List; @Repository public class FruitMemoryRepository implements FruitRepository{ private final JdbcTemplate jdbcTemplate; private List memory = new ArrayList(); private int num = 0; public FruitMemoryRepository(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } public void saveFruit(String name, long price, LocalDate warehousingDate) { memory.add(new Fruit(num+1,name, warehousingDate,price,1)); } public void saleFruit(int id){ for (Fruit e : memory){ if (e.getId() == id) e.setSaled(0); else throw new IllegalArgumentException(); } } public List overviewFruit(String name){ List list = new ArrayList(); for (Fruit e : memory){ if (e.getName().equals(name)){ list.add(e); }else throw new IllegalArgumentException(); } return list; } }위 클래스는 클래스 내에 List 를 만들어 저장함으로써, 프로그램을 재실행시키면, 기존 정보가 없어지는 repository이다. db와 연결이 없다.FruitMySqlRepository.javapackage com.group.libraryapp.repository; import com.group.libraryapp.domain.Fruit; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; import java.time.LocalDate; import java.util.List; @Repository @Qualifier("sql") public class FruitMySqlRepository implements FruitRepository{ private final JdbcTemplate jdbcTemplate; public FruitMySqlRepository(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } public void saveFruit(String name, long price, LocalDate warehousingDate) { String sql = "INSERT INTO fruits(name, warehousingDate, price,saled) Values (?,?,?,?)"; jdbcTemplate.update(sql, name, warehousingDate,price, 1); } public void saleFruit(int id){ String readSql = "SELECT * FROM fruits WHERE id = ?"; boolean fruitNotExist = jdbcTemplate.query(readSql, (rs,rowNum)-> 0,id).isEmpty(); if (fruitNotExist){ throw new IllegalArgumentException(); } //fruit exists String sql = "UPDATE fruits SET saled = ? WHERE id = ?"; jdbcTemplate.update(sql, 0,id); } public List overviewFruit(String name){ String readSql = "SELECT * FROM fruits WHERE name = ?"; List list = jdbcTemplate.query(readSql, (rs, rowNum) -> { String rs_name = rs.getString("name"); long rs_price = rs.getLong("price"); LocalDate rs_warehousingDate = rs.getDate("warehousingDate").toLocalDate(); int rs_saled = rs.getInt("saled"); return new Fruit(rs_name,rs_warehousingDate,rs_price,rs_saled); }, name); if (list.isEmpty()) throw new IllegalArgumentException(); return list; } }위 부분은 반대로 db와의 연결이 있고, 직접적인 sql 문을 사용하여 작성하였다. @Qualifier 어노테이션이 클래스 위에 붙어있는 것을 확인할 수 있다.회고controller 클래스만 쓰면, 여러 기능이 뭉쳐있어 코드의 가독성이 떨어지는데,위와 같이 3단 분리 및 interface 를 활용하니, 전보다는 클린코드로 작성이 된 것 같다.
백엔드
・
워밍업
・
1기
・
6차
・
과제
・
백엔드
2024. 05. 05.
0
[인프런 워밍업 클럽 스터디1기] 백엔드 - 1주차 회고
1주일간의 백엔드 공부를 정리해 보려한다.자바와 스프링 부트로 생애 최초 서버 만들기, 누구나 쉽게 개발부터 배포까지! [서버 개발 올인원 패키지]위 강의를 듣고 1주일간 배운 것은 restful API 작성법,이를 database와 연결하는 법,람다식과 익명 클래스 차이에 대한 것이다. 이번주는 집중을 잘 못했다. 다른 일이 겹쳐서 강의 듣는 것 조차 버거웠는데,현재 13강까지 마쳤으며,과제는 1차,3차를 제출한 상태이다. 과제 기간이 2~3일 마다 1번인 것을 감안할 때,매일 강의를 2개씩 듣고,이에 대한 정리를 노션에 짧게 한다음,평일에 1번 , 주말에 1번 정리하는 식으로 내 개별 블로그에 쓰고,이를 각각 발자국 글로 작성 후,회고 형식으로 발자국 글을 쓰려 한다.별도로 과제에 대한 글도 개별 블로그에 쓰려한다. ot와 Q&A 를 참여했었는데Q&A에서 프로젝트 주제 선정은 자신이 가고 싶은 회사의 어플리케이션 혹은 관련 부분을 구현하는게좋을 것 같다는 답변을 듣고이 강의와 스터디를 마치고 어떤 프로젝트를 구현하면 좋을지 생각중이다.전반적으로 이번주는 스터디에 대한 적응을 하고 강의 초입부를 들은 상태이다.다음주도 힘내서 참여해야겠다.
백엔드
・
워밍업
・
1기
・
1주차
・
회고
・
백엔드
2024. 05. 05.
0
[인프런 워밍업 클럽 스터디1기] 백엔드 - 3차 과제
진도표 3일차와 연결됩니다우리는 JdbcTemplate을 사용하는 과정에서 익명 클래스와 람다식이라는 자바 문법을 사용했습니다. 익명 클래스는 자바의 초창기부터 있던 기능이고, 람다식은 자바 8에서 등장한 기능입니다. 다음 키워드를 사용해 몇 가지 블로그 글을 찾아보세요! 아래 질문을 생각하며 공부해보면 좋습니다! 😊 [키워드]익명 클래스 / 람다 / 함수형 프로그래밍 / @FunctionalInterface / 스트림 API / 메소드 레퍼런스 [질문]자바의 람다식은 왜 등장했을까?람다식과 익명 클래스는 어떤 관계가 있을까? - 람다식의 문법은 어떻게 될까? 자바의 람다식 등장 배경람다식이 등장하게 된 이유는 불필요한 코드를 줄이고, 가독성을 높이기 위함이다.현재 실습하고 있는 '도서관리 애플리케이션'의 getmapping api 코드에서 람다식으로 바꾸기 전과 후를 비교해보자.처음에는 database를 연결하지 않았을 때 getmapping 상태이다. database 연결 안된 상태 이를 database를 연결하면 아래와 같이 코드를 쓸 수 있다. 이때는 람다식을 적용하지 않았다. 람다식 적용하지 않고 database 연결 그후 람다식을 적용한 코드이다. 람다식 적용 겉으로는 new RowMapper 객체 선언과 @override 하여 정의하는 부분이 없어졌다. 따라서 더 간결해진 코드를 만들 수 있어 가독성이 높아진다.람다식과 익명 클래스는 어떤 관계먼저 람다식 사용법에 대해 알아보자.생략 가능한 부분이 많기 때문에 헷갈릴 수 있지만 대부분(매개변수) -> {실행코드}형식으로 작성된다.(예시)public interface Functional { int cal(int a, int b); } public interface Functional2 { void print(String str); } public interface Functional3 { void noArgs(); } // 1. 작성 가능한 모든 내용을 생략 없이 작성한 경우 Functional2 functional2 = (String name) -> { System.out.println(name) }; // 2. 매개변수의 타입을 생략한 경우 Functional2 functional2 = (name) -> { System.out.println(name) }; // 3. 매개변수가 한개여서 소괄호를 생략한 경우 Functional2 functional2 = name -> { System.out.println(name) }; // 4. 실행 코드가 한 줄이어서 중괄호를 생략한 경우 Functional2 functional2 = name -> System.out.println(name); // 5. 매개변수가 없어서 소괄호를 생략할 수 없는 경우 Functional3 functional3 = () -> System.out.println("functiona3"); // 6. 반환값이 있는 경우 return 키워드 사용하는 경우 Functional functional = (a, b) -> { System.out.println("print"); return a+b; }; // 7. 실행 코드가 반환 코드만 존재하는 경우 키워드와 중괄호 생략한 경우 Functional functional = (a, b) -> a+b; 그렇다면 익명 클래스는 무엇일까?프로그램에서 일시적으로 한번만 사용되고 버려지는 객체이므로 재사용이 되지 않아 이름이 없는 클래스이다.즉, 딱 한번만 인스턴스를 생성하기 위해서 선언되는 것이다. 익명 클래스(익명 객체)를 구현하는 방법은 아래와 같다.// 선언 방법 인터페이스명 변수명 = new 인터페이스명() { 인터페이스의 메서드 오버라이딩 } // MessengerTest.java public class MessengerTest { public static void main(String[] args) { /* 일반 클래스의 인스턴스 생성과 사용 */ GalaxyMessenger galaxy = new GalaxyMessenger(); System.out.println(galaxy.getMessage()); galaxy.setMessage("Hi, I'm Galaxy."); galaxy.changeKeyboard(); /* 익명 클래스의 선언과 사용 */ Messenger anonymousTest = new Messenger() { // 익명 클래스 선언 @Override public String getMessage() { // 인터페이스 메서드 오버라이딩 return "anonymousTest"; } @Override public void setMessage(String msg) { // 인터페이스 메서드 오버라이딩 System.out.println("anonymousTest에서 메시지를 설정합니다: " + msg); } }; // 익명 클래스의 멤버 사용 System.out.println(anonymousTest.getMessage()); anonymousTest.setMessage("Have a nice day!"); } }GalaxyMessenger 클래스는 앞서 표시한 Messenger 인터페이스를 구현하는 일반 클래스이다. new 명령문을 이용하여 인스턴스를 생성하였다.익명 클래스 또한 new 명령문을 이용하여 인스턴스를 생성하는데, 이 때 참조타입을 상속하는 인터페이스(혹은 클래스)로 지정해야한다는 것에 유의하자.익명 클래스의 멤버 사용 방법은 일반 클래스와 다르지 않다. 참조변수로 접근하여 사용한다. 그렇다면, 람다식과 익명 클래스는 어떤 관계가 있을까? 익명 함수와 매우 비슷하지만 불필요한 선언부들이 모두 생략되어 있고, 인터페이스 함수의 구현부만 정의하고 있어 코드가 가장 간결하다 즉, 같은 역할을 할 수 있지만 람다식이 더 간단하게 쓸 수 있는 것이다. [출처]https://wonyong-jang.github.io/java/2021/03/12/Java-Lambda-Expressions.html https://limkydev.tistory.com/226 https://velog.io/@balparang/Java-%EC%9D%B5%EB%AA%85-%ED%81%B4%EB%9E%98%EC%8A%A4Anonymous-Class
백엔드
・
백엔드
・
워밍업
・
1기
・
3차
・
과제
2024. 05. 05.
0
[인프런 워밍업 클럽 스터디1기] 백엔드 - OT
학교 단톡방에서 우연히 백엔드 클럽을 모집한다는 소식을 접하고 바로 신청했다.백엔드에 대한 기초없이 프로젝트를 하게 되었었는데되게 힘들어서 이에 대한 기초를 다지고자 공부할 수 있는 것들을 찾아보던 중나에게 딱 필요한 강의여서 우선 신청했고,현직 개발자님의 코치 및 Q&A도 받을 수 있다해서 바로 등록했다. OT 는 온라인으로 진행되었다. 완주 러너의 기준에 대한 정보를 받았는데약간 빡세다는 점이 좋았고,매일 강의를 들어야 스터디 클럽을 완주할 수 있을 것 같아서열심히 참여해야겠구나 생각했다. 또한 디스코드 채널도 세분화되어있어서 공부할때나필요한 정보가 있을때 잘 활용할 수 있을 것 같아서좋았다. 앞으로 열심히 해야겠다.
백엔드
・
인프런
・
백엔드
・
워밍업
・
1기
・
스터디