블로그
전체 102024. 03. 10.
0
[인프런 워밍업 클럽 0기] BE 3주차 발자국
일주일 간의 학습내용객체지향JPA 연관관계미니 프로젝트3주차를 진행하면서, 2주차에 이어 JPA에 대해 추가적인 학습을 하게되었다. 객체지향과 데이터베이스의 형태는 다르기 때문에 JPA의 연관관계 @OneToMany, @ManyToOne의 키워드에 대해 학습하였다. 또한 3주차를 진행하면서 미니프로젝트 STep1까지의 프로젝트를 만들어보았으며 추후 Step 2, 3, 4를 해볼 예정이다. 금요일 라이브 회고금요일 라이브에서는 코치님의 코드리뷰 라이브를 보았다.모두에게 공개되는 자신의 코드에 부끄럼없이 보여주신분들에게 감사를 표한다. 이번 코드리뷰에서는 더욱 객체지향적인 코드를 만드는 방법은 물론이며, 코치님이 생각하는 클린코드에 대해 듣게되었다. 우선 가장 기억에 남는부분은 Service 계층이 뚱뚱해지는 것을 막으려면, 도메인이 처리할 수 있는 부분은 도메인에 구현하라였다. 그렇게 도메인에 책임을 부여하게되면 Service 계층이 가벼워지고, 더욱 읽기 쉬운 코드가 되었다는 부분이었다. 두번째로는, Enum을 이용하여 Role 부분의 확장을 생각해보는 부분이었다. Enum을 자주쓰지 않았기 때문에, 어떤식으로 써야할지 감이 잘 오지 않았지만, 이번에 리뷰를 들으며 나의 프로젝트에서도 Enum을 사용하여 리팩토링을 해볼 예정이다. 마지막으로는, 헥사고날 아키텍쳐에 대해 간단하게 들었던 부분이다. 현재 널리사용되고 있는 Layered Architecture는 데이터베이스에 너무 의존적이기 때문에, 현재 코드 그대로 데이터베이스만 바꿀수 없지만, 헥사고날 아키텍쳐는 데이터베이스만 바꾸는 것이 가능하다. 얘기만 들었을때는, 추상화에 더 신경쓴 아키텍쳐라 생각되어 코드를 작성할 때에는 시간적인 비용이 많이들겠지만, 설계를 다 마치고나면 유지보수와 코드를 읽는데에는 훨씬 좋을 것 이라고 생각된다. 이번 워밍업 클럽을 통해 발전한 자신을 볼 수 있었으며, 좋은 기회를 주신 인프런과 최태현 코치님께 감사의 말씀을 올리며 마무리하겠습니다. 감사합니다
백엔드
2024. 03. 03.
0
[인프런 워밍업 클럽 0기] BE 2주차 발자국
일주일 간의 학습내용스프링 컨테이너JPA트랜잭션6일차: 스프링 컨테이너의 의미와 사용 방법스프링 컨테이너가 하는 역할이 무엇인지, 그로인해 얻을 수 있는 장점이 무엇인지 학습하였다.7일차: Spring Data JPA를 사용한 데이터베이스 조작Spring이 제공하는 Spring Data JPA를 이용하여 편리하게 CRUD를 구성해보았다8일차: 트랜잭션과 영속성 컨텍스트@Transactional 어노테이션의 학습과 JPA의 영속성 컨텍스트를 이용해 캐시의 개념처럼 사용한다는 것을 알게되었다.9일차: 조금 더 복잡한 기능을 API로 구성하기JPA를 이용해 API로 만들어 보았다. 2주차 미션6일차: Controller, Service, Repository 계층구조에 대해 익숙해지기https://www.inflearn.com/blogs/68557일차: 과제 6에서 만들었던 Fruit을 JPA로 리팩토링하기https://www.inflearn.com/blogs/6895 회고2주차를 진행하면서 Spring의 기본적인 원리와 JPA의 간단한 사용방법을 알아보았다. 다음주엔 미니프로젝트를 진행해 볼 차례인데 지금까지 배웠던걸 잘 녹여내서 미니프로젝트를 만들어볼 것 이다.
2024. 02. 27.
0
[인프런 워밍업 클럽] BE 0기 7일차 과제
Fruit)import javax.persistence.*; import java.time.LocalDate; @Entity public class Fruit { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(length = 30) private String name; private LocalDate warehousingDate; private long price; private boolean isSold; protected Fruit(){} public Fruit(String name, LocalDate warehousingDate, long price) { this.name = name; this.warehousingDate = warehousingDate; this.price = price; this.isSold = false; } public Long getId() { return id; } public String getName() { return name; } public LocalDate getWarehousingDate() { return warehousingDate; } public long getPrice() { return price; } public boolean isSold() { return isSold; } public void setSold(boolean sold) { isSold = sold; } }primary key인 id 설정을 해주었으며, Entity로 사용하기 위해 빈 생성자를 만들어 주었다. Fruit 테이블 상태)요청을 위한 FruitController 메소드) @GetMapping("/api/v1/fruit/count") public CountResponse countFruit(@RequestParam String name) { return fruitService.countFruit(name); }FruitService 메소드)@Service public class FruitService { private final FruitRepository fruitRepository; private final FruitJpaRepository jpaRepository; public FruitService(FruitRepository fruitRepository, FruitJpaRepository jpaRepository) { this.fruitRepository = fruitRepository; this.jpaRepository = jpaRepository; } public CountResponse countFruit(String name) { List fruits = jpaRepository.findAllByName(name); return new CountResponse(fruits.size()); } } 기존 FruitService에서 변경점이 생겼다. JpaRepository사용을 위해 FruitJpaRepository 인터페이스를 생성자로 받아줬으며, 인터페이스의 메소드를 정의 후 사용하였다.결과)정상적으로 나오는걸 확인할 수 있었다.문제를 확인했을 때, 스프링 데이터 JPA의 쿼리문을 이용하여 메소드명을 작성해야 할 것으로 생각하였다. 또한 응답 body의 형태는 List의 형태를 취했으며 List를 사용하였다.컨트롤러) @GetMapping("/api/v1/fruit/list") public List notSoldFruitList(FruitOptionRequest request) { return fruitService.notSoldFruitList(request); }서비스) public List notSoldFruitList(FruitOptionRequest request) { return getFruits(request).stream() .map(FruitResponse::new) .collect(Collectors.toList()); } private List getFruits(FruitOptionRequest request) { if (request.getOption().equals("GTE")) { System.out.println("hi"); return jpaRepository.findAllByPriceGreaterThanEqualAndIsSoldFalse(request.getPrice()); } else { return jpaRepository.findAllByPriceLessThanEqualAndIsSoldFalse(request.getPrice()); } }request option으로 넘어온 데이터를 확인하기 위한 메소드를 만들어 작성하였다. 그 결과로 주 목적인 notSoldFruitList의 코드는 간결해졌으며, 서비스 계층에서 중요한 역할인 비즈니스를 만을 위한 코드를 작성 해주었다. FruitJpaRepository)import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; public interface FruitJpaRepository extends JpaRepository { List findAllByName(String name); List findAllByPriceGreaterThanEqualAndIsSoldFalse(Long price); List findAllByPriceLessThanEqualAndIsSoldFalse(Long price); }이상을 위한 GreatherThanEqual, 이하를 위한 LessThanEqual을 사용해주었으며, IsSold의 boolean 값 체크를 위해 False를 이어주었다.결과)GTE, LTE 모두 정상적인 값을 출력하였다.
2024. 02. 26.
0
[인프런 워밍업 클럽] BE 0기 6일차 과제
컨트롤러)@RestController public class FruitController { private final FruitService fruitService; public FruitController(FruitService fruitService) { this.fruitService = fruitService; } @PostMapping("/api/v1/fruit") public void saveFruit(@RequestBody FruitRequest request) { fruitService.saveFruit(request); } @PutMapping("/api/v1/fruit") public void saleFruit(@RequestBody SoldOutFruitRequest request) { fruitService.saleFruit(request); } @GetMapping("/api/v1/fruit/stat") public TotalPriceResponse salesData(@RequestParam String name) { return fruitService.salesData(name); } }서비스)@Service public class FruitService { private final FruitRepository fruitRepository; public FruitService(FruitRepository fruitRepository) { this.fruitRepository = fruitRepository; } public void saveFruit(FruitRequest request) { fruitRepository.saveFruit(request); } public void saleFruit(SoldOutFruitRequest request) { boolean isEmpty = fruitRepository.checkSoldOut(request); if (isEmpty) { throw new IllegalArgumentException(); } fruitRepository.saleFruit(request); } public TotalPriceResponse salesData(String name) { return new TotalPriceResponse(fruitRepository.salesRead(name), fruitRepository.notSalesRead(name)); } } 레포지토리 인터페이스)public interface FruitRepository { void saveFruit(FruitRequest request); void saleFruit(SoldOutFruitRequest request); boolean checkSoldOut(SoldOutFruitRequest request); long salesRead(String name); long notSalesRead(String name); } FruitMemoryRepository)@Primary @Repository public class FruitMemoryRepository implements FruitRepository { private final List fruits = new ArrayList(); @Override public void saveFruit(FruitRequest request) { fruits.add(new Fruit(request.getName(), request.getWarehousingDate(), request.getPrice())); } @Override public void saleFruit(SoldOutFruitRequest request) { Fruit fruit = fruits.get((int) request.getId()); fruit.setSold(true); } @Override public boolean checkSoldOut(SoldOutFruitRequest request) { Fruit fruit = fruits.get((int) request.getId()); if (fruit.isSold()) { return true; } return false; } @Override public long salesRead(String name) { long price = 0; for (int i = 0; i 결과)메모리에 저장하는 방식으로서, 서버를 재시작 할때마다 데이터를 입력해 주어야 한다. 초기 데이터 입력으로 사과 5000, 사과 7000, 사과 9000을 입력해주었고, 사과 7000을 판매했을 때 판매 데이터 결과값이다. FruitMySqlRepository)@Repository public class FruitMySqlRepository implements FruitRepository { private final JdbcTemplate jdbcTemplate; public FruitMySqlRepository(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } @Override public void saveFruit(FruitRequest request) { String sql = "insert into fruit (name, warehousing_date, price) values (?, ?, ?)"; jdbcTemplate.update(sql, request.getName(), request.getWarehousingDate(), request.getPrice()); } @Override public void saleFruit(SoldOutFruitRequest request) { String sql = "update fruit set is_sold = true where id = ?"; jdbcTemplate.update(sql, request.getId()); } @Override public boolean checkSoldOut(SoldOutFruitRequest request) { String readSql = "select id from fruit where id = ? and is_sold = false"; return jdbcTemplate.query(readSql, (rs, rowNum) -> 0, request.getId()).isEmpty(); } @Override public long salesRead(String name) { String salesRead = "select sum(price) price from fruit where name = ? and is_sold = true"; List price = jdbcTemplate.query(salesRead, (rs, rowNum) -> rs.getLong("price"), name); return price.get(0); } @Override public long notSalesRead(String name) { String notSalesRead = "select price from fruit where name = ? and is_sold = false group by price"; List price = jdbcTemplate.query(notSalesRead, (rs, rowNum) -> rs.getLong("price"), name); return price.get(0); } }계층구조 리팩토링을 진행한 결과로서, 4일차 과제와 동일한 결과값을 나타내주었다.이번 리팩토링을 진행함으로서 controller, service, repository 계층별 역할과 책임에 대해 자세히 알게 되었고, 인터페이스를 적용시킬때가 언제 인지를 확실하게 알게 되었다. 추가적으로, FruitMemoryRepository에서 스트림, 람다식, 메소드 레퍼런스를 사용해보았다. @Override public long salesRead(String name) { return fruits.stream() .filter(fruit -> fruit.getName().equals(name)) .filter(Fruit::isSold) .mapToLong(Fruit::getPrice) .sum(); } @Override public long notSalesRead(String name) { return fruits.stream() .filter(fruit -> fruit.getName().equals(name)) .filter(fruit -> !fruit.isSold()) .mapToLong(fruit -> fruit.getPrice()) .sum(); }salesRead에서 메소드 레퍼런스를 적용시켰고, notSalesRead에서는 람다식을 이용해서 코드를 작성해보았다.3일차 과제에서 배웠던 내용을 적용시킴으로 한층 더 발전된 코드가 되었다.
2024. 02. 25.
0
[인프런 워밍업 클럽 0기] BE 1주차 발자국
신청 이유스프링 공부를 했지만 내가 스프링을 잘 다룰 수 있을까? 라는 생각이 들던차에, 이번 워밍업 클럽을 보게 됐습니다. 제가 가지고 있는 지식을 활용하여 커리큘럼을 따라가고, 따라가는 과정에서 주어지는 과제를 직접 코드로 구현하고, 이론적인 내용을 학습하기 위해 신청을 하게 됐습니다. 일주일 간의 학습내용GET, POST, PUT API 만들기Database 테이블 생성 및 테이블 수정클린코드1일차: 서버 개발을 위한 환경 설정 및 네트워크 기초스프링 부트 초기설정 방법에 대해 공부하고, HTTP에 대해 간략하게 학습하였다. 2일차: 첫 HTTP API 개발GET,POST API를 이용해 유저 생성 및 유저 조회 API를 개발하였다.3일차: 기본적인 데이터베이스 사용법유저 리스트를 휘발성 메모리에 담아 서버가 꺼지면 없어지는 방식을 데이터베이스를 이용해 서버가 꺼지더라도 비휘발성 메모리에 담게 저장방식을 바꾸었다.4일차: 데이터베이스를 사용해 만드는 API유저 업데이트 API, 유저 삭제 API 개발과 예외 처리를 진행하였다.5일차: 클린코드의 개념과 첫 리팩토링프로젝트의 유지보수를 위해서 클린코드는 필수적이다. 클린코드란 무엇이고 기존의 Controller를 Service와 Repository 계층으로 나누어 리팩토링 하였다. 1주차 미션1일차: 어노테이션 이란?https://www.inflearn.com/blogs/65402일차: GET, POST API 개발https://www.inflearn.com/blogs/65783일차: 람다식, 익명클래스, 함수형 프로그래밍 이란?https://www.inflearn.com/blogs/66484일차: 데이터베이스를 이용한 API 개발https://www.inflearn.com/blogs/66815일차: 클린코드를 위한 코드 리팩토링https://www.inflearn.com/blogs/6709회고1주차를 진행하면서, 나에게 어떤 부분이 부족했는지 더욱 와닿는 시간을 가졌다. 과제를 직접 개발해보고, 더 좋은 방법은 없을까 생각을 하면서 진행했던 부분이 다양한 방법을 생각하게 해주었으며 람다와 스트림과 같은 함수형 프로그래밍과 같은 기법도 조금 더 연습이 필요 하다는 것을 알게 되었다. 차주 강의에서도 이러한 부분에 대해 고민하며, 과제를 수행할 시 부족한 부분에 대해 더 학습하여 내 것으로 만든다는 느낌을 가지고 진행해 보도록 하겠습니다.
2024. 02. 23.
0
[인프런 워밍업 클럽] BE 0기 5일차 과제
클린코드를 위해 각 책임에 맞게 객체들을 만들어 보았다. Main, InputOutput, CountingMainpublic class Main { public static void main(String[] args) { InputOutput io = new InputOutput(); Counting count = new Counting(); int a = io.numberInput(); count.count(a); io.output(count); } }InputOutputimport java.util.Scanner; public class InputOutput { public int numberInput(){ System.out.print("숫자를 입력하세요 : "); Scanner scanner = new Scanner(System.in); return scanner.nextInt(); } public void output(Counting counting) { for (int i = 0; i Countingpublic class Counting { public final static int DICENUM = 6; private int[] arr = new int[DICENUM]; public void count(int a) { for (int i = 0; i 입출력을 위한 객체를 만들어 입출력만을 위한 책임을 부여했으며, Counting 객체를 만들어 배열을 만든 뒤, 나온 숫자에 따라 횟수를 증가시켰으며, 원하는 숫자의 횟수가 몇 번인지 알기 위해 getter를 수정하였습니다. 현재 Counting 객체는 DICENUM 상수를 통하여 간단한 변수설정을 위해 주사위 면의 갯수를 설정하고, 그에 따라 배열 설정과 출력이 가능하게 하였습니다. 하지만 이 방법은 여러 면의 주사위를 사용하거나, 전략패턴을 사용할 때와 같이, 몇 개의 면을 가진 주사위를 사용할지 런타임에 결정되게 할 때에는 적합하지 않습니다.위와 같은 문제의 해결 방법으로 생각해본 것은, 인터페이스를 만들어 count 메소드를 선언하며, 각 구현체를 주사위의 면을 사용할 개수만큼 만들어 구현하여 기존에는 불가능하였던 확장이 가능해집니다. 하지만 단점도 존재합니다. 초기 코드를 작성할 때 여러개의 Counting 구현체를 만들어야 하여 코드를 작성하는 것이 번거로워 프로젝트의 크기를 생각할 때 trade-off를 생각하면, 인터페이스를 만든 뒤 구현체를 만드는 방식보단, 상수설정을 통하여 한개의 변수를 건드는 것 만으로 원하는 대로 기능이 작동되게 하였습니다. 주사위 면이 6개일 때의 결과 (DICENUM = 6)주사위 면이 12개일 때의 결과 (DICENUM = 12)