게시글
블로그
전체 142024. 03. 17.
3
인프런 워밍업 클럽 후기
[인프런 워밍업 클럽 참여 후기]부트캠프를 마치고 독학 중인데, 제대로 공부하고 있는지 의문이 많았습니다. 그래서 인프런 워밍업 클럽에 참여해보기로 결정했습니다. 이 스터디에 참여한 경험은 정말 값진 것이었습니다.먼저, 진도표를 따라 매일 강의를 수강하고 발자국을 작성하는 것이 매우 도움이 되었습니다. 주말에는 과제를 마무리하고 발자국을 작성하는 시간을 가지면서, 내가 어떤 것을 배웠는지 정리할 수 있었습니다. 이를 통해 제 학습 내용을 정리하고 실제로 습득한 지식을 확인할 수 있었습니다.또한, 온라인 라이브 세션에서는 코치의 특강이나 Q&A 세션을 통해 중간 점검을 받을 수 있었습니다. 코치와 함께 진도를 되돌아보고 질문에 대한 답변을 듣는 것은 매우 유익했습니다. 그리고 스터디의 끝을 알리는 수료식에서는 이 스터디를 통해 얼마나 발전했는지를 되돌아보는 소중한 시간을 가졌습니다. (온라인으로 수료식을 참가해서 아쉬웠습니다 ㅠ)가장 인상 깊었던 점은 코치님들이 직접 제작한 미션을 수행하는 경험했었습니다. 이를 통해 배운 내용을 다시 한 번 복습하고 응용하는 기회를 가질 수 있었습니다. 또한, 다른 참가자들의 발자국을 보면서 서로의 학습 경험을 공유할 수 있어서 좋았습니다.총론적으로, 인프런 워밍업 클럽에 참여한 것은 매우 값진 경험이었습니다. 부트캠프를 마치고도 지식을 유지하고 발전시키기 위한 좋은 방법이라고 생각합니다. 다른 분들에게도 적극 추천하고 싶습니다.유익한 지식을 주신 코치님들, 스터디 클럽을 위해 힘써주신 인프런 분들, 같이 달린 학생들, 또한 이 글을 읽어주신 분들 모두 감사합니다.
인프런
・
인프런워밍업클럽
・
스터디0기
2024. 03. 08.
0
과제 _ 미니 프로젝트
자바로 미니 프로젝트를 만들어보려고 한다.팀 등록 기능 : 회사에 있는 팀을 등록해야하고 '팀 이름'을 필수적으로 가져야한다.직원 등록 기능 : 직원을 등록할 수 있다. '직원 이름' , '팀의 매니저인지 매니저가 아닌지 여부','회사에 들어온 일자', '생일' 이라는 정보를 필수적으로 가져야 한다.팀 조회 기능 : 모든 팀의 정보를 한 번에 조회할 수 있어야 한다.[{"name" : "팀 이름", "manager" : "팀 매니저 이름" (없으면 null), "memberCount" : 팀 인원 수[숫자]}]직원 조회 기능 : 모든 직원의 정보를 한 번에 조회할 수 있어야 한다.[{"name" : "직원 이름", "teamName" : "소속 팀 이름", "role" : "MANAGER" or "MEMBER", "birthday" : "1997-08-18", "workStartDate" : "2024-01-01"}]먼저 프로젝트를 생성하고 객체 지향적 접근 방식을 사용한다.다음과 같은 형식의 프로젝트를 만들었다.https://github.com/soo1e/inflearn_MiniProject코드는 여기서 확인할 수 있다.팀 등록 기능직원 등록 기능팀 조회 기능직원 조회 기능다음처럼 모든 조건을 다 만족시켰다!회고록음 인프런 워밍업 클럽 스터디를 3주동안 진행해봤다. 큰 프로젝트의 구조와 흐름에 대해 알 수 있어서 좋은 시간이었다. 백엔드 개발에 대한 전반적인 중요한 지식들을 얻어갈 수 있어 좋았다. 이러한 배운 걸로 개인 프로젝트를 더욱 진행해서 유능한 개발자가 되기 위해 노력해야겠다. 3월은 대부분의 공고들이 나오는 시즌이다. 이 스터디를 들은 모든 인원이 좋은 결과가 있기를 바라며!
인프런워밍업클럽
2024. 03. 03.
0
과제 7. JPA
1. JPA 이용해서 바꾸기package com.group.libraryapp.domain.fruit; import javax.persistence.*; import java.time.LocalDate; @Entity @Table(name = "fruits") public class Fruit { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false) private String name; @Column(name = "warehousing_date", nullable = false) private LocalDate warehousingDate; @Column(nullable = false) private double price; @Column(nullable = false) private String status; public Long getId() { return id; } public String getName() { return name; } public LocalDate getWarehousingDate() { return warehousingDate; } public double getPrice() { return price; } public String getStatus() { return status; } public void setId(Long id) { this.id = id; } public void setName(String name) { this.name = name; } public void setWarehousingDate(LocalDate warehousingDate) { this.warehousingDate = warehousingDate; } public void setPrice(double price) { this.price = price; } public void setStatus(String status) { this.status = status; } }이렇게 fruit 객체를 만들었다.ckage com.group.libraryapp.domain.fruit; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; public interface FruitRepository extends JpaRepository { @Query("SELECT SUM(f.price) FROM Fruit f WHERE f.name = :name AND f.status = :status") Double getFruitStat(@Param("name") String name, @Param("status") String status); Fruit findByName(String name); } 그리고 JPA 레포지토리를 확장하는 FruitRepository를 새로 만들어줬다.package com.group.libraryapp.service.fruit; import com.group.libraryapp.domain.fruit.Fruit; import com.group.libraryapp.domain.fruit.FruitRepository; import com.group.libraryapp.dto.HW.FruitRequest; import com.group.libraryapp.dto.HW.FruitSoldRequest; import com.group.libraryapp.dto.HW.FruitStatResponse; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class FruitService { private final FruitRepository fruitRepository; public FruitService(FruitRepository fruitRepository) { this.fruitRepository = fruitRepository; } @Transactional public FruitRequest addFruit(FruitRequest fruitRequest) { Fruit fruit = new Fruit(); fruit.setName(fruitRequest.getName()); fruit.setWarehousingDate(fruitRequest.getWarehousingDate()); fruit.setPrice(fruitRequest.getPrice()); fruit.setStatus("NOT_SOLD"); fruitRepository.save(fruit); return fruitRequest; } @Transactional public void markFruitAsSold(FruitSoldRequest request) { Fruit fruit = fruitRepository.findById(request.getId()).orElseThrow(IllegalArgumentException::new); fruit.setStatus("SOLD"); fruitRepository.save(fruit); } @Transactional(readOnly = true) public FruitStatResponse getFruitStat(String name) { Double soldAmount = fruitRepository.getFruitStat(name, "SOLD"); Double notSoldAmount = fruitRepository.getFruitStat(name, "NOT_SOLD"); long sold = soldAmount != null ? soldAmount.longValue() : 0; long notSold = notSoldAmount != null ? notSoldAmount.longValue() : 0; return new FruitStatResponse(sold, notSold); } }그리고 이렇게 서비스를 만들어 기존의 코드를 SQL 문이 아닌 JPA를 이용하게 했다.2. 과일 개수 세기FruitRepository에서 과일의 개수를 세는 메소드를 추가해야 한다. 그런 다음에는 FruitService에서 해당 메소드를 호출하여 원하는 기능을 구현할 수 있다.package com.group.libraryapp.domain.fruit; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; public interface FruitRepository extends JpaRepository { Fruit findByName(String name); @Query("SELECT COUNT(f) FROM Fruit f WHERE f.name = :name") long countByName(@Param("name") String name); } package com.group.libraryapp.service.fruit; import com.group.libraryapp.domain.fruit.Fruit; import com.group.libraryapp.dto.HW.FruitStatResponse; import com.group.libraryapp.repository.fruit.FruitRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class FruitService { private final FruitRepository fruitRepository; @Autowired public FruitService(FruitRepository fruitRepository) { this.fruitRepository = fruitRepository; } @Transactional(readOnly = true) public long countFruitsByName(String name) { return fruitRepository.countByName(name); } } Copy code @GetMapping("/count") public ResponseEntity> countFruitsByName(@RequestParam("name") String name) { long count = fruitService.countFruitsByName(name); Map responseBody = new HashMap(); responseBody.put("count", count); return ResponseEntity.ok(responseBody); }이렇게 만들고 PostMan을 실행해봤다.이렇게 잘 된다!3. 판매되지 않은 특정 금액 이상 혹은 특정 금액 이하의 과일 목록package com.group.libraryapp.domain.fruit; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import java.util.List; public interface FruitRepository extends JpaRepository { @Query("SELECT SUM(f.price) FROM Fruit f WHERE f.name = :name AND f.status = :status") Double getFruitStat(@Param("name") String name, @Param("status") String status); @Query("SELECT COUNT(f) FROM Fruit f WHERE f.name = :name") long countByName(@Param("name") String name); List findByPriceGreaterThanEqualAndStatus(double price, String status); List findByPriceLessThanEqualAndStatus(double price, String status); Fruit findByName(String name); }먼저 FruitRepository를 다음처럼 GTE, LTE를 만들어줬다.ckage com.group.libraryapp.service.fruit; import com.group.libraryapp.domain.fruit.Fruit; import com.group.libraryapp.domain.fruit.FruitRepository; import com.group.libraryapp.dto.HW.FruitListResponse; import com.group.libraryapp.dto.HW.FruitRequest; import com.group.libraryapp.dto.HW.FruitSoldRequest; import com.group.libraryapp.dto.HW.FruitStatResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.stream.Collectors; @Service public class FruitService { private final FruitRepository fruitRepository; @Autowired public FruitService(FruitRepository fruitRepository) { this.fruitRepository = fruitRepository; } @Transactional public FruitRequest addFruit(FruitRequest fruitRequest) { Fruit fruit = new Fruit(); fruit.setName(fruitRequest.getName()); fruit.setWarehousingDate(fruitRequest.getWarehousingDate()); fruit.setPrice(fruitRequest.getPrice()); fruit.setStatus("NOT_SOLD"); fruitRepository.save(fruit); return fruitRequest; } @Transactional public void markFruitAsSold(FruitSoldRequest request) { Fruit fruit = fruitRepository.findById(request.getId()).orElseThrow(IllegalArgumentException::new); fruit.setStatus("SOLD"); fruitRepository.save(fruit); } @Transactional(readOnly = true) public FruitStatResponse getFruitStat(String name) { Double soldAmount = fruitRepository.getFruitStat(name, "SOLD"); Double notSoldAmount = fruitRepository.getFruitStat(name, "NOT_SOLD"); long sold = soldAmount != null ? soldAmount.longValue() : 0; long notSold = notSoldAmount != null ? notSoldAmount.longValue() : 0; return new FruitStatResponse(sold, notSold); } @Transactional(readOnly = true) public long countFruitsByName(String name) { return fruitRepository.countByName(name); } @Transactional(readOnly = true) public List listFruitsByPriceOption(String option, double price) { if (option.equalsIgnoreCase("GTE")) { return fruitRepository.findByPriceGreaterThanEqualAndStatus(price, "NOT_SOLD"); } else if (option.equalsIgnoreCase("LTE")) { return fruitRepository.findByPriceLessThanEqualAndStatus(price, "NOT_SOLD"); } else { throw new IllegalArgumentException("Invalid option value. Use 'GTE' or 'LTE'."); } } @Transactional(readOnly = true) public List getFruitsWithPriceGreaterThanOrEqual(double price) { List fruits = fruitRepository.findByPriceGreaterThanEqualAndStatus(price, "NOT_SOLD"); return fruits.stream() .map(this::mapToFruitListResponse) .collect(Collectors.toList()); } @Transactional(readOnly = true) public List getFruitsWithPriceLessThanOrEqual(double price) { List fruits = fruitRepository.findByPriceLessThanEqualAndStatus(price, "NOT_SOLD"); return fruits.stream() .map(this::mapToFruitListResponse) .collect(Collectors.toList()); } private FruitListResponse mapToFruitListResponse(Fruit fruit) { FruitListResponse response = new FruitListResponse(); response.setName(fruit.getName()); response.setPrice(fruit.getPrice()); response.setWarehousingDate(fruit.getWarehousingDate()); return response; } }서비스 부분도 이러한 것들을 추가해서 리스트를 뽑아내게 해줬다.@GetMapping("/list") public ResponseEntity> getFruitsByPriceOption( @RequestParam("option") String option, @RequestParam("price") double price) { List fruits; if ("GTE".equals(option)) { fruits = fruitService.getFruitsWithPriceGreaterThanOrEqual(price); } else if ("LTE".equals(option)) { fruits = fruitService.getFruitsWithPriceLessThanOrEqual(price); } else { return ResponseEntity.badRequest().build(); } return ResponseEntity.ok(fruits); }그리고 path를 컨트롤러에 추가해서 작동하게 해줬다.그러니 이렇게 다음처럼 잘 작동했다!
인프런워밍업클럽
2024. 03. 03.
0
과제 6. 코드 분리하기
1. 과제 4에서 만들었던 API를 분리해보자// Controller package com.group.libraryapp.controller.HW; import com.group.libraryapp.dto.HW.FruitRequest; import com.group.libraryapp.dto.HW.FruitSoldRequest; import com.group.libraryapp.dto.HW.FruitStatResponse; import com.group.libraryapp.service.fruit.FruitService; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/v1/fruit") public class FruitController { private final FruitService fruitService; public FruitController(FruitService fruitService) { this.fruitService = fruitService; } @PostMapping public FruitRequest addFruit(@RequestBody FruitRequest fruitRequest) { return fruitService.addFruit(fruitRequest); } @PutMapping public ResponseEntity markFruitAsSold(@RequestBody FruitSoldRequest request) { fruitService.markFruitAsSold(request); return ResponseEntity.ok().build(); } @GetMapping("/stat") public FruitStatResponse getFruitStat(@RequestParam("name") String name) { return fruitService.getFruitStat(name); } } // Service package com.group.libraryapp.service.fruit; import com.group.libraryapp.dto.HW.FruitRequest; import com.group.libraryapp.dto.HW.FruitSoldRequest; import com.group.libraryapp.dto.HW.FruitStatResponse; import com.group.libraryapp.repository.fruit.FruitRepository; import org.springframework.stereotype.Service; @Service public class FruitService { private final FruitRepository fruitRepository; public FruitService(FruitRepository fruitRepository) { this.fruitRepository = fruitRepository; } public FruitRequest addFruit(FruitRequest fruitRequest) { fruitRequest.setStatus("NOT_SOLD"); fruitRepository.addFruit(fruitRequest.getName(), fruitRequest.getWarehousingDateAsString(), fruitRequest.getPrice(), fruitRequest.getStatus()); return fruitRequest; } public void markFruitAsSold(FruitSoldRequest request) { fruitRepository.markFruitAsSold(request.getId()); } public FruitStatResponse getFruitStat(String name) { long soldAmount = Math.round(fruitRepository.getFruitStat(name, "SOLD")); long notSoldAmount = Math.round(fruitRepository.getFruitStat(name, "NOT_SOLD")); return new FruitStatResponse(soldAmount, notSoldAmount); } } // Repository package com.group.libraryapp.repository.fruit; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; @Repository public class FruitRepository { private final JdbcTemplate jdbcTemplate; public FruitRepository(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } public void addFruit(String name, String warehousingDate, double price, String status) { String sql = "INSERT INTO fruits (name, warehousingDate, price, status) VALUES (?, ?, ?, ?)"; jdbcTemplate.update(sql, name, warehousingDate, price, status); } public void markFruitAsSold(long id) { String sql = "UPDATE fruits SET status = 'SOLD' WHERE id = ?"; jdbcTemplate.update(sql, id); } public double getFruitStat(String name, String status) { String sql = "SELECT SUM(price) as totalAmount FROM fruits WHERE name = ? AND status = ?"; return jdbcTemplate.queryForObject(sql, Double.class, name, status); } } 기존에 만들었던 API를 수업처럼 Controller - Service - Repository로 분리했다.2. 문제 1에서 분리된 코드들 중 FruitRepository를 FruitMemoryRepository와 FruitMySqlRepository로 나누고 @Primary 어노테이션을 활용해보자.package com.group.libraryapp.repository.fruit; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Repository; @Primary @Repository public class FruitMemoryRepository implements FruitRepository { @Override public void addFruit(String name, String warehousingDate, double price, String status) { // 메모리에 과일 추가하는 로직 } @Override public void markFruitAsSold(long id) { // 메모리에서 과일 상태를 'SOLD'로 변경하는 로직 } @Override public double getFruitStat(String name, String status) { // 메모리에서 특정 과일의 통계를 계산하는 로직 return 0; // 예시로 0으로 반환 } } package com.group.libraryapp.repository.fruit; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; @Repository public class FruitMySqlRepository implements FruitRepository { private final JdbcTemplate jdbcTemplate; public FruitMySqlRepository(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } @Override public void addFruit(String name, String warehousingDate, double price, String status) { String sql = "INSERT INTO fruits (name, warehousingDate, price, status) VALUES (?, ?, ?, ?)"; jdbcTemplate.update(sql, name, warehousingDate, price, status); } @Override public void markFruitAsSold(long id) { String sql = "UPDATE fruits SET status = 'SOLD' WHERE id = ?"; jdbcTemplate.update(sql, id); } @Override public double getFruitStat(String name, String status) { String sql = "SELECT SUM(price) as totalAmount FROM fruits WHERE name = ? AND status = ?"; return jdbcTemplate.queryForObject(sql, Double.class, name, status); } } 이렇게 Memory와 Mysql 부분으로 레포지토리를 나누고 기본 프라이머리 값을 FruitMemoryRepository로 설정했다!
인프런워밍업클럽
2024. 03. 03.
0
섹션 5. 책 요구사항 구현하기
책 생성, 대출, 반납 API를 온전히 개발하며 지금까지 다루었던 모든 개념을 실습해본다.객체지향적으로 설계하기 위한 연관관계를 이해하고, 연관관계의 다양한 옵션에 대해 이해한다.JPA에서 연관관계를 매핑하는 방법을 이해하고, 연관관계를 사용해 개발할 때와 사용하지 않고 개발할 때의 차이점을 이해한다.책 생성 APIHTTP Method -> POSTHTTP Path -> /bookHTTP Body{ "name" : String } // 책 이름 결과 반환은 하지 않는다.기본적으로 DTO, 컨트롤러, Repository 등을 만들고 웹으로 들어가서 확인해보자.대출 기능 API사용자가 책을 빌릴 수 있다. 단, 다른 사람이 그 책을 빌렸다면 빌릴 수 없다.POST, /book/loan, {"userName":String, "bookName":String}, 결과 반환은 따로 없다.요구 사항을 보니 지금 테이블로는 요구 사항이 충분하지 않다. 왜냐하면 대출이 되었는지 여부에 대해서 알 수 없고 user 정보에 대해서도 이를 알 수 없다.그렇기 위해 새로운 Table을 추가하자.그 후 UserLoanHistory라는 객체를 만들어주었다. 또한, 똑같이 DTO, Controller, Service를 구현해주었다.반납 기능 API대출한 책을 반납하는 API를 만들어보자.API 스펙과 HTTP Body가 대출과 완전히 동일한데 이럴 경우 어떻게 하는게 좋을까?DTO를 새로 만드는 것이 좋을까 아니면 재활용 하는 것이 좋을까?새로 만드는 것을 추천한다. 왜냐하면 그래야 두 기능 중 한 기능에 변화가 생겼을 때 유연하고 side-effect 없이 대처할 수 있기 때문이다.또한 테이블에 대해서도 고민을 해보자.User와 UserLoanHistory가 직접 협업하게 처리를 해보자.대출기능을 다음과 같이 개선할 수 있다. 또한 반납 기능도 아래처럼 개선할 수 있다. JPA 연관관계에 대한 추가적인 기능상대 테이블을 가리키는 테이블이 연관관계의 주인이다. 연관관계의 주인이 아닌 객체는 mappedBy를 통해 주인에게 매여 있음을 표시해주자.양쪽 모두 연관관계를 갖고 있을 때는 양쪽 모두 한 번에 맺어주는게 좋다.cascade 옵션을 활용하면, 저장이나 삭제를 할 때 연관관계에 놓인 테이블까지 함께 저장 또는 삭제가 이루어진다.orphanRemoval 옵션을 통해 연관관계가 끊어진 데이터를 자동으로 제거해준다.이번주 회고: JPA 부분이나 컨테이너 부분 쪽은 큰 지식이 없었어서 따라가기 살짝 벅찼다. 기존의 코드를 각자의 역할로 분리해서 만들어보고 이를 JPA나 트랜잭션을 통해 더욱 깔끔하게 만들었다. 어려운 부분이 많았다. 이러한 점을 실습을 통해서 어느정도 갈증을 해소할 수 있었지만 그럼에도 공부할 부분이 많다고 느꼈다. 미니 프로젝트까지 화이팅이다.
인프런워밍업클럽
2024. 03. 03.
0
섹션 4. 생애 최초 JPA 사용하기
현재는 외부에서 API 호출을 하면 스프링 컨테이너가 빈들을 관리하고 DB는 MySQL과 연동되어 있다.SQL을 직접 작성하면 어떤 점이 아쉬울까?-> 문자열을 직접 작성하기 때문에 실수할 수 있고, 실수를 인지하는 시점이 느리다. 컴파일 시점에 발견되지 않고, 런타임 시점에 오류가 발생한다.또한, 특정 데이터베이스에 종속적이게 된다. 그리고 테이블을 만들 때마다 CRUD 쿼리가 필요해서 반복 작업이 많아지게 된다.데이터베이스의 테이블과 객체는 패러다임이 다르다.그래서 JPA가 등장하게 되었다.JPA는 데이터를 영구적으로 보관하기 위해 Java 진영에서 정해진 규칙이다.객체와 관계형 DB의 테이블을 짝지어 데이터를 영구적으로 저장할 수 있도록 정해진 Java 진영의 규칙이라고 생각하자!Hibernate는 JPA - 규칙을 구현하고 JDBC를 사용해서 자바에서 데이터베이스를 사용할 수 있게 해준다.이전에 만든 User 객체를 사용해서 진행해보자.기존의 데이터베이스의 user 테이블에는 자동 증가하는 id, name, age가 있었다. 현재 객체의 User에는 name과 age만 존재하므로 id를 추가해주자.@Id // id annotation -> 이 필드를 primary key로 간주한다 @GeneratedValue(strategy = GenerationType.IDENTITY) // 자동 증가 (auto_increment) private Long id = null; @Column : 객체의 필드와 Table의 필드를 매칭한다.@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 생략 가능 private Integer age; protected User() { }이렇게 완전히 매핑에 성공했다. jpa: hibernate: ddl-auto: none properties: hibernate: show_sql: true format_sql: true dialect: org.hibernate.dialect.MySQL8Dialect그리고 yaml 파일에 의존성을 추가해줬다.이제 기본 세팅을 끝났다. 이제 SQL을 작성하지 않고 CRUD 기능을 수행해보자.먼저 UserRepository를 User 객체 옆으로 옮겨주자. 기존의 UserRepository를 UserJdbcRepository로 바꿔줬다. 그리고 인터페이스로 UserRepository를 만들고 JpaRepository를 상속 받아야 한다.// UserRepository public interface UserRepository extends JpaRepository { } // 유저 저장 기능 public void saveUser(UserCreateRequest request) { userRepository.save(new User(request.getName(), request.getAge())); } // 유저 조회 기능 public List getUsers() { List users = userRepository.findAll(); return users.stream().map(user -> new UserResponse(user.getId(), user.getName(), user.getAge())).collect(Collectors.toList()); }save 메소드에 객체를 넣어주면 INSERT SQL이 자동으로 날라간다!그리고 stream을 이용해서 findAll을 사용하면 모든 데이터를 가져온다. 이를 통해 유저 조회 기능도 만들었다.유저 업데이트 기능은 id를 이용해 있는지 없는지 확인하고 존재한다면 update 쿼리를 통해 데이터를 수정한다.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); if (user == null) { throw new IllegalArgumentException(); } userRepository.delete(user); }반환 타입은 User이며 함수 이름을 findByName으로 작성하면 알아서 조립되어서 이름에 따라 찾을 수 있게 된다. 물론 반환 할 것이 없으면 Null을 반환한다.이렇게 모든 기능을 JPA를 사용해서 바꿔봤다.트랜잭션트랜잭션이랑 쪼갤 수 없는 업무의 최소 단위를 의미한다.public class OrderService { public void completePayment() { orderRepository.save(new Order()); pointRepository.save(new Order()); billingHistoryRepository.save(new BillingHistory()); } }만약 이런 주문 서비스에서 에러가 나면 어떻게 처리할까? 예를 들면 포인트가 적립이 되지 않는다거나 주문은 했으나 영수증이 나오지 않는 등의 에러가 발생한다면 어떻게 처리해야할까? 그럴 때는 하나라도 실패하면 모두 실패시키고 모든 SQL을 성공시키거나 2가지로 나눠서 생각한다. 이것이 트랜잭션이다.트랜잭션을 시작하고 commit을 해주어야 해당 과정이 끝나게 된다.트랜잭션을 우리 코드에 어떻게 적용시킬 수 있을까?우리가 원하는 것은 서비스 메서드가 시작할 때 트랜잭션이 시작되어 서비스 메서드 로직이 정상 성공하면 commit, 아니라면 rollback해주는 것이다.@Transactional을 사용하면 가능하다!@org.springframework.transaction.annotation.Transactional public void saveUser(UserCreateRequest request) { userRepository.save(new User(request.getName(), request.getAge())); throw new IllegalArgumentException(); }추가적으로 SELECT 쿼리만 사용한다면 readonly 옵션을 쓸 수 있다.주의할 점은 IOException은 롤백되지 않는다.영속성 컨텍스트란?테이블과 매핑된 Entity 객체를 관리/보관하는 역할스프링에서는 트랜잭션을 사용하면 영속성 컨텍스트가 생겨나고, 트랜잭션이 종료되면 영속성 컨텍스트가 종료된다.영속성 컨텍스트의 특수 능력변경 감지 -> 영속성 컨텍스트 안에서 불러와진 엔티티는 명시적으로 save하지 않더라도, 변경을 감지해 자동으로 저장된다.쓰기 지연 -> DB의 INSERT, UPDATE, DELETE SQL을 바로 날리는 것이 아니라 트랜잭션이 commit 될 때 된다.1차 캐싱 -> ID를 기준으로 Entity를 기억한다.또한 이렇게 캐싱된 객체는 완전 동일하다.다음 section에서 ^_^
인프런워밍업클럽
2024. 03. 03.
0
섹션 3. 역할의 분리와 스프링 컨테이너
현재 Controller의 문제점현재는 API 진입 지점으로써 HTTP Body를 객체로 변환하고 있다.현재 유저가 있는지 없는지 확인하고 예외 처리를 해준다.SQL을 사용해 실제 DB와의 통신을 담당한다.이를 삼단 분리해보자1번은 Controller2번은 Service3번은 Repository로 분리해보자.// UserService public class UserService { // 현재 유저가 있는지 없는지 확인하고 예외처리를 해주는 부분 public void updateUser(JdbcTemplate jdbcTemplate, UserUpdateRequest request) { String readSql = "SELECT * FROM user Where id = ?"; boolean isUserNotExist = jdbcTemplate.query(readSql, (rs, rowNum) -> 0, request.getId()).isEmpty(); if (isUserNotExist) { throw new IllegalArgumentException(); } String sql = "UPDATE user SET name = ? WHERE id = ?"; jdbcTemplate.update(sql, request.getName(), request.getId()); } } // Controller @PutMapping("/user") public void updateUser(@RequestBody UserUpdateRequest request) { userService.updateUser(jdbcTemplate, request); }// Repository public class UserRepository { // SQL을 사용해 실제 DB와 통신한다 public boolean isUserNotExist(JdbcTemplate jdbcTemplate, long id) { String readSql = "SELECT * FROM user Where id = ?"; return jdbcTemplate.query(readSql, (rs, rowNum) -> 0, id).isEmpty(); } public void updateUserName(JdbcTemplate jdbcTemplate, String name, long id) { String sql = "UPDATE user SET name = ? WHERE id = ?"; jdbcTemplate.update(sql, name, id); } } // Service public class UserService { // 현재 유저가 있는지 없는지 확인하고 예외처리를 해주는 부분 private final UserRepository userRepository = new UserRepository(); public void updateUser(JdbcTemplate jdbcTemplate, UserUpdateRequest request) { boolean isUserNotExist = userRepository.isUserNotExist(jdbcTemplate, request.getId()); if (isUserNotExist) { throw new IllegalArgumentException(); } userRepository.updateUserName(jdbcTemplate, request.getName(), request.getId()); } }이렇게 분리를 했다.Controller의 하나의 코드를 Controller, Service, Repository로 바꿨다.현재는 JdbcTemplate를 Repository만 사용하는데 코드는 모든 코드에 있다. 이를 클린하게 바꿔보자.또한 다른 코드도 위의 삼단 분리를 해보자.예를 들어 getUser API를 분리해보면 먼저 Repository에 다음과 같이 만들어준다.public List getUsers() { String sql = "SELECT * FROM user"; return jdbcTemplate.query(sql, new RowMapper() { @Override public UserResponse mapRow(ResultSet rs, int rowNum) throws SQLException { long id = rs.getLong("id"); String name = rs.getString("name"); int age = rs.getInt("age"); return new UserResponse(id, name, age); } }); }이걸 이제 UserService에서 이용할 수 있게 해준다.public List getUsers(){ return userRepository.getUsers(); }그리고 본 API에서는 /user에서 userService에서 가져오도록 한다.@GetMapping("/user") public List getUsers() { return userService.getUsers(); }그런데 Repository에서 JdbcTemplate을 어떻게 가져올수는 없을까?현재 UserController의 의아한 점은 누가 UserController을 인스턴스화 해주고 있냐는 것이다.또한, UserController는 JdbcTemplate가 없으면 동작하지 않는다. 근데 우리는 JdbcTemplate를 설정해 준 적이 없는데 어떻게 가져온 것일까?답은 @RestController에 있다.UserController 클래스를 스프링 빈으로 등록시킨다.스프링 빈스프링 빈이란 무엇일까?먼저 서버가 실행되면 스프링 서버 내부에 거대한 컨테이너를 만들게 된다. 컨테이너 안에는 여러 클래스가 들어가게 된다. 이 때, 다양한 정보도 함께 들어있고, 인스턴스화도 이루어진다.스프링은 이러한 빈들을 관리하고 제공하는데, 일반적으로 이들은 스프링 컨테이너에 의해 생성되고 관리된다. 빈은 일반적으로 Java 클래스로 표현되며, 애플리케이션의 다양한 부분에서 사용되는 객체다.스프링에서 빈은 스프링의 의존성 주입(Dependency Injection) 기능을 통해 구성되며, 이를 통해 빈들 간의 의존성을 관리하고 애플리케이션의 유연성과 테스트 용이성을 높일 수 있다.UserRepository는 스프링 빈이 아니기 때문에 바로 JdbcTemplate를 가져오지 못한 것이었다.이를 해결하려면 UserRepository를 빈으로 등록해주면 된다.@Repository, Service를 통해 어노테이션으로 등록해준다.이제 다음 순서를 따른다.먼저 스프링 빈들이 등록이 된다. 그리고 UserRepository가 등록되고 UserService가 등록이 된다. 또한 UserController도 이후에 등록이 된다.그런데 스프링 컨테이너를 왜 사용하는 걸까?스프링 컨테이너를 사용하면 컨테이너가 Service를 대신 인스턴스화 하고, 그 때 알아서 Repository를 결정해준다.컨테이너가 선택해 Service에 Repository를 선택해서 넣어주는 과정을 의존성 주입 (DI, Dependency Injection)라고 한다.이런 방식을 제어의 역전이라고 한다.스프링 컨테이너는 객체 간의 의존성을 관리하고 제어한다. 이를 통해 개발자는 객체를 직접 생성하고 관리하지 않고, 스프링 컨테이너에게 객체 생성 및 주입을 위임함으로써 객체 간의 결합도를 낮출 수 있다.빈을 등록하는 방법@Configuration클래스에 붙이는 어노테이션@Bean을 사용할 때 함께 사용해 주어야 한다.@Bean메소드에 붙이는 어노테이션메소드에서 반환되는 객체를 스프링 빈에 등록한다.
인프런워밍업클럽
2024. 02. 24.
0
과제 5. 클린 코드
현재의 코드를 클린하게 바꿔보자.우리는 라는 개념을 배웠습니다. 에 대한 감각을 익히기 위해서는 어떤 코드가 좋은 코드이고, 어떤 코드가 좋지 않은 코드인지 이론적인 배경을 학습하는 것도 중요할 뿐 아니라, 다양한 코드를 읽어 보며 어떤 부분이 읽기 쉬웠는지, 어떤 부분이 읽기 어려웠는지, 읽기 어려운 부분은 어떻게 고치면 좋을지 경험해보는 과정이 필요합니다.이번 과제는 제시된 코드를 읽어보며, 코드를 더 좋은 코드로 고쳐나가는 과정입니다. 구글에 “클린 코드” 혹은 “클린 코드 정리”를 키워드로 검색해보면, 이론적인 배경을 충분히 찾아보실 수 있습니다. 🙂 그러한 내용들을 보며 제시된 코드를 더 좋은 코드로 바꿔보세요! (코드를 바꿀 때 왜 바뀐 코드가 더 좋은 코드인지 다른 사람에게 설명하신다고 생각해보시면 더욱 좋습니다.)한걸음 더!현재 코드는 주사위가 1 ~ 6이라는 가정으로 작성되어 있는데 주사위의 눈의 수가 12나 20처럼 크다면 바꿔야 할 게 더 많아진다. 이러한 불편을 줄여보자import java.util.Scanner; public class Main { public static void main(String[] args) { System.out.print("숫자를 입력하세요 : "); Scanner scanner = new Scanner(System.in); int a = scanner.nextInt(); int r1 = 0, r2 = 0, r3 = 0, r4 = 0, r5 = 0, r6 = 0; for (int i = 0; i =0 && b = 1 && b = 2 && b = 3 && b = 4 && b = 5 && b 현재는 일일히 다 코드를 직접 치고 있다. 또한 랜덤 난수의 값도 일일히 체크해서 범위에 들어오는지 찾고 있다. 나는 이러한 점을 고치기 위해 먼저 배열을 사용해보자고 생각했다. 주사위의 눈의 수만큼의 크기를 가진 배열을 만들고 이것들을 0번 인덱스에는 1번 눈이 나온 횟수, 1번 인덱스에는 2번 눈이 나온 횟수....를 끝까지 해서 각 인덱스의 수를 프린트해주면 될 것이라고 생각했다. 또한 주사위의 눈의 수를 처음부터 스캐너에 입력받아서 그 수를 사용해서 반복문을 실행하면 좋을 것 같다고 생각했다.import java.util.Scanner; public class Main { public static void main(String[] args) { System.out.println("숫자를 입력하세요 : "); Scanner scanner = new Scanner(System.in); int a = scanner.nextInt(); System.out.println("주사위의 눈의 수를 입력하세요 : "); int dice_eyes = scanner.nextInt(); int[] array = new int[dice_eyes]; for (int i = 1; i 먼저 난수의 범위에 +1을 더한 후, 정수화해줘서 나온 주사위의 수를 배열에 담았다. 또한 이것을 주사위의 눈만큼 반복해서 몇 번의 횟수를 출력하는지 뽑아줬다.
인프런워밍업클럽
2024. 02. 24.
0
과제 4. API
1. 과일 가게에 입고된 과일 정보를 저장하는 api를 만들어보자!method는 post를 쓰고 path는 /api/v1/fruit을 이용한다.create table fruits ( id bigint auto_increment, name varchar(25), warehousingDate date, price bigint, status varchar(10) CHECK (status IN ('SOLD', 'NOT_SOLD')) );// DTO package com.group.libraryapp.dto.HW; import java.time.LocalDate; public class FruitRequest { private String name; private LocalDate warehousingDate; private long price; private String status; public FruitRequest(String name, LocalDate warehousingDate, long price, String status) { this.name = name; this.warehousingDate = warehousingDate; this.price = price; this.status = status; } public String getName() { return name; } public LocalDate getWarehousingDate() { return warehousingDate; } public long getPrice() { return price; } public void setStatus(String status) { this.status = status; } public String getStatus() { return status; } }과일 가게에 입고된 과일 정보를 저장하는 API를 만들기 위해 먼저 과일 정보를 담을 수 있는 요청 바디를 정의하는 DTO 클래스를 생성했다.DTO를 사용하여 POST 요청을 처리하는 컨트롤러를 만들었다.@Autowired private JdbcTemplate jdbcTemplate; @PostMapping("/api/v1/fruit") public FruitRequest addFruit(@RequestBody FruitRequest fruitRequest) { // 기본 상태를 'NOT_SOLD'로 설정 fruitRequest.setStatus("NOT_SOLD"); String sql = "INSERT INTO fruits (name, warehousingDate, price, status) VALUES (?, ?, ?, ?)"; jdbcTemplate.update(sql, fruitRequest.getName(), fruitRequest.getWarehousingDate(), fruitRequest.getPrice(), fruitRequest.getStatus()); return fruitRequest; // 저장된 과일 정보 반환 }다음과 같이 잘 작동한다!한 걸음 더!위 API에서 long을 사용한 이유는 무엇일까?-> 과일 가격과 같이 큰 정수가 될 수 있는 경우에 long을 사용하는 것이 좋을 것 같다. 과일 가격이 매우 높거나, 가격을 특정 단위로 표현할 때 int의 최대값을 초과할 수 있기 때문이다.2. 과일이 팔리게 되면, 우리 시스템에 팔린 과일 정보를 기록해야 한다.// DTO package com.group.libraryapp.dto.HW; public class FruitSoldRequest { private long id; // 기본 생성자를 명시적으로 추가 public FruitSoldRequest() { } // id 필드에 대한 세터 메서드 public void setId(long id) { this.id = id; } // id 필드에 대한 게터 메서드 public long getId() { return id; } } @PutMapping("/api/v1/fruit") public ResponseEntity markFruitAsSold(@RequestBody FruitSoldRequest request) { String sql = "UPDATE fruits SET status = 'SOLD' WHERE id = ?"; int updatedRows = jdbcTemplate.update(sql, request.getId()); if (updatedRows == 0) { return ResponseEntity.notFound().build(); } return ResponseEntity.ok().build(); }이렇게 보내고 나면 상태가 아래처럼 SOLD로 바뀐다.3. 우리는 특정 과일을 기준으로 팔린 금액, 팔리지 않은 금액을 조회하고 싶다.(1, 사과, 3000원, 판매 O)(2, 사과, 4000원, 판매 X)(3, 사과, 3000원, 판매 O)데이터베이스를 다음과 같이 예시처럼 바꿔주었다.// DTO package com.group.libraryapp.dto.HW; public class FruitStatResponse { private long salesAmount; private long notSalesAmount; // 생성자 public FruitStatResponse(long salesAmount, long notSalesAmount) { this.salesAmount = salesAmount; this.notSalesAmount = notSalesAmount; } // 게터 및 세터 public long getSalesAmount() { return salesAmount; } public void setSalesAmount(long salesAmount) { this.salesAmount = salesAmount; } public long getNotSalesAmount() { return notSalesAmount; } public void setNotSalesAmount(long notSalesAmount) { this.notSalesAmount = notSalesAmount; } } @GetMapping("/api/v1/fruit/stat") public FruitStatResponse getFruitStat(@RequestParam("name") String name) { String sql = "SELECT status, SUM(price) as totalAmount FROM fruits WHERE name = ? GROUP BY status"; AtomicLong salesAmount = new AtomicLong(0); AtomicLong notSalesAmount = new AtomicLong(0); jdbcTemplate.query(sql, new Object[]{name}, (rs, rowNum) -> { String status = rs.getString("status"); long totalAmount = rs.getLong("totalAmount"); if ("SOLD".equals(status)) { salesAmount.addAndGet(totalAmount); } else if ("NOT_SOLD".equals(status)) { notSalesAmount.addAndGet(totalAmount); } return null; }); return new FruitStatResponse(salesAmount.get(), notSalesAmount.get()); }이렇게 DTO와 컨트롤러를 설정해주고 POSTMAN을 통해 실행해봤다.다음처럼 상태에 따라 팔린 금액과 팔리지 않은 금액이 잘 반환되었다!
인프런워밍업클럽
2024. 02. 24.
0
과제 3. 람다식
1. 자바의 람다식은 왜 등장했을까?자바의 람다식은 더 직관성이 좋은 코드를 만들기 위해 생겼다고 생각한다. 비동기 처리나 컬렉션 처리, 이벤트 리스너등에서 FP 스타일을 지원하기 위해 사용되는 람다식은 명령형 프로그래밍과 함수형 프로그래밍의 장점을 모두 이용할 수 있도록 도와준다.익명 클래스의 생성 없이도 메서드를 간단하고 직관적으로 표현할 수 있어 코드가 간결해지고 다른 언어처럼 함수형 프로그래밍이 가능해지고 이런 직관성은 코드 유지 관리의 효율을 높인다고 생각한다.2. 람다식과 익명 클래스는 어떤 관계가 있을까? - 람다식의 문법은 어떻게 될까?람다식과 익명 클래스는 익명의 구현체를 사용한다. 이를 통해 간결한 코드를 만든다. 하지만 람다식이 좀 더 직관성이 좋은 깔끔한 코드를 만들 수 있다. 익명 클래스보다 문법이 더 단순하며 함수형 프로그래밍의 개념을 위해 설계 되었기 때문에 함수형 인터페이스의 인스턴스를 간결하게 만든다.// 기존 코드 interface Calculator { int sum(int a, int b); } class MyCalculator implements Calculator { public int sum(int a, int b) { return a+b; } } public class Sample { public static void main(String[] args) { MyCalculator mc = new MyCalculator(); int result = mc.sum(3, 4); System.out.println(result); // 7 출력 } } // 람다식을 이용한 코드 interface Calculator { int sum(int a, int b); } public class Sample { public static void main(String[] args) { Calculator mc = (int a, int b) -> a +b; // 람다를 적용한 코드 int result = mc.sum(3, 4); System.out.println(result); } } 참고 : Jump to Java
인프런워밍업클럽