블로그
전체 92024. 03. 04.
0
인프런 워밍업 클럽 0기 - 백엔드 코스 (3주차 회고)
인프런 워밍업 클럽 3주차까지 마무리했습니다. 이번 주에는 JPA를 객체지향적으로 사용하는 방법과 실제 서버를 배포하는 방법, 설정 파일들에 대해 배울 수 있었습니다.하나의 강의를 들으면서 프로젝트 설정부터 개발, 실제 배포까지 배울 수 있어 알찬 구성이었던 것 같습니다.마지막 회고를 통해 배운 내용을 정리해보도록 하겠습니다. 10일차이전에 JPA를 활용해 개발을 했을 때 테이블의 연관관계를 그저 외래키를 Long타입으로 가지고 있는 형식으로 사용했습니다. 이렇게 사용하는 것은 그저 Table을 매핑해주는 역할로만 Jpa를 활용하고 있는 상태입니다.그러므로 테이블과 연결된 엔티티를 좀 더 객체지향적인 방식으로 사용할 수 있는 방법에 대해 알아보았습니다.테이블의 연관관계를 정의할 때 1:1, 1:N, N:M의 관계를 갖게 됩니다. 이를 좀 더 객체지향적인 방법으로 각 엔티티가 다른 엔티티를 연결시켜줄 수 있습니다.A, B엔티티가 1:1 연관관계를 가질 경우를 예로 들면, A 엔티티 클래스 안에 멤버 변수로 연결된 엔티티 B를 선언해주고 @OneToOne을 통해 1:1 연관관계를 맺고 있다는 것을 알려줍니다. 이 후 연관관계를 맺은 두 엔티티 중 연관관계의 주인을 정해주면 됩니다.연관관계의 주인은 해당 관계의 우위를 점하고 있다는 뜻이 아니라 어떤 엔티티가 외래키를 관리할지 결정해주는 것입니다.테이블에서는 연관관계를 외래키를 통해 맺고 단방향 연관관계만을 가집니다. 하지만, 엔티티와 같은 경우 연관관계를 갖는 엔티티가 양방향 연관관계를 맺을 경우가 있습니다. 이럴 경우 하나의 엔티티에서 연관관계를 끊거나 다른 엔티티로 변경되었을 경우 이를 실제 DB 테이블에 변경사항을 저장해야 합니다. 이럴 경우 연관관계의 주인을 통해 외래키를 변경하거나 제거함으로써 연관관계를 수정하도록 하는 것입니다.연관관계의 주인은 @JoinColumn으로 선언해줍니다. 양방향 연관관계를 가지고 연관관계의 주인이 아닌 쪽에는 mappedBy를 통해 연관관계의 주인을 알려줍니다. @OneToOne과 @ManyToOne은 Fetch 전략이 EAGER로 되어 있습니다. EAGER전략은 엔티티를 조회하면 연관된 엔티티도 함께 조인을 통해 조회하게 됩니다. 이럴 경우 의도치 않은 join문이 발생하기도 하고, 또한 가장 안 좋은 경우인 N+1개의 쿼리가 발생하는 경우도 발생하게 됩니다. 그렇기 때문에 우선 Fetch 전략은 모두 LAZY로 변경하는 것이 최적화를 위한 좋은 방법입니다. 11일차현재 서버는 로컬에 배포된 상태로 이 서버에 접근하기 위해서는 실제 제가 있는 집에 방문해 보는 것 밖에 없습니다.그러므로 이 서버를 실제 여러 사람들에게 공개하기 위해서는 다른 사람들이 접근할 수 있는 컴퓨팅 시스템에 이를 노출시켜야 합니다. 이러한 컴퓨팅 자원을 원하는 만큼 제공해주는 것을 클라우드 서비스 중 하나인 IaaS라고 합니다.IaaS를 제공하는 회사 중 하나인 AWS의 무료 인스턴스를 통해 웹 서버 배포를 시도해보았습니다. 우선 웹 서버의 ip를 할당받고 포트포워딩을 통해 ssh 접속을 시도하거나 웹서버 접근을 위한 포트를 열어줍니다.또한, 빌린 인스턴스에서 서버를 동작시키기 위해서는 현재 작성한 코드를 전부 인스턴스로 이동시켜야 합니다. 모든 코드를 수동으로 전송하는 것은 굉장히 비효율적인 작업이므로 이를 해소하기 위해서 분산 버전 관리 툴인 Git과 GitHub를 이용해보도록 하겠습니다. 12일차AWS를 통해 EC2 인스턴스를 받은 다음, 배포에 필요한 Git, Java, MySQL 등을 설치해줍니다.그리고 GitHub를 통해 지금까지 작성한 코드를 git pull을 통해 받습니다.인스턴스 내에서 코드를 빌드하고 실행시킨다면 해당 인스턴스의 ip주소를 통해 접속이 가능해집니다.또한, 백그라운드 실행을 통해 해당 인스턴스에 대한 ssh접속이 끊어져도 실행이 계속되도록 유지시킬 수 있습니다. 13일차build.gradle의 여러 설정들에 대해 알아보았습니다.Spring 프레임워크를 사용하기 위해서는 트랜잭션, 톰캣, 다른 라이브러리들을 위한 설정들을 xml문법으로 작성해주어야 했습니다. 이러한 불편함을 해소하기 위해 여러 설정들을 자동으로 도와주고, 메이저 라이브러리일 경우 starter를 통해 버전을 작성하지 않더라도 알아서 호환되는 버전으로 사용하도록 도와주는 Spring Boot가 사용되고 있습니다. Spring Boot는 xml 뿐만 아니라 yaml이라는 마크업 언어를 통해 설정을 작성할 수 있습니다. 롬복은 getter나 setter 등 반복적으로 나타나는 코드를 어노테이션 형식으로 지원해주는 기능입니다. Spring Boot나 다른 프레임워크의 버전 업을 시도할 경우 변경해줘야 하는 부분은 무엇인지 차근히 확인하고 변경해주는 것이 좋을 것이다.Spring Boot가 3.0.0으로 넘어오면서 변경된 점은 Java의 최신 버전이 17로 업그레이드 되었고 javax 대신 jakarta 패키지를 사용하게 됐습니다. 이렇게 변경된 점을 IDE나 build.gradle, 실제 코드에 모두 반영시켜야 되기 때문에 신중하게 작업해야될 것 같습니다. 미니 프로젝트https://github.com/beomseok37/practice-spring-boot/tree/main/company 14일차 & 진짜 후기이렇게 모든 강의를 마무리하게 되었습니다. 전반적인 백엔드 개발에 필요한 지식들을 얻어갈 수 있어 좋았습니다.저와 같은 경우에는 이전에 JPA에 관련된 강의도 조금 수강을 한터라 배운 내용을 점검하는 방식으로 진행했던 것 같습니다.Spring Boot를 활용하면서 Spring에 대한 정보보다도 다른 데이터베이스, 리눅스 문법, 설정 파일 등 다양한 지식을 얻을 수 있어 좋았습니다.이후에는 현재 배운 내용들과 함께 좀더 동적으로 쿼리를 다룰 수 있는 방법을 통해 개인 프로젝트를 진행하면서 실제 스프링 동작 원리에 대해서도 깊게 파해쳐보는 시간을 가져보려고 합니다.
백엔드
・
인프런워밍업클럽
・
3주차회고
・
백엔드코스
2024. 02. 29.
0
인프런 워밍업 클럽 0기 - 백엔드 코스 (2주차 회고)
인프런 2주차까지 모든 강의와 과제들을 마무리했습니다.1주차에 학습했던 내용과 함께 스프링 컨테이너, Spring Data Jpa, 트랜잭션 등 스프링과 DB 개념 그리고 스프링과 DB 매핑 방법과 같이 전반적인 백엔드 기술들을 배울 수 있었습니다. 이후에는 이를 활용한 다른 요구사항 개발과정들을 다시 복습할 수 있는 과정으로 이어져 배운 내용들을 활용해볼 수 있는 기회를 가질 수 있었습니다. 6일차이전에 모든 계층이 JdbcTemplate이나 다른 계층의 객체에 의존적으로 설계되어 있었습니다. 이러한 구조는 의존할 객체가 변경될 경우 의존할 객체를 변경해주어야 되므로, 객체지향 개발 원리 중 OCP와 DIP를 위반하고 있습니다. 그렇기 때문에 스프링이 제공하는 스프링 컨테이너를 활용해 클래스 내부에서 사용할 다른 클래스들을 스프링 빈으로 등록하고, 필요할 때 등록된 빈을 자동 주입받는 형식으로 사용하도록 변경되었습니다.이럴 경우 인터페이스의 구현체만 사용자가 변경해주면 자동적으로 스프링이 주입할 빈을 선택하여 자동 주입해주기 때문에 스프링 빈을 사용하는 구간에서는 코드의 변경이 발생하지 않고 사용할 수 있습니다.사용할 인터페이스에 구현체가 여러 개일 경우 @Primary 어노테이션이나 @Qualifer를 통해 자동 주입 받을 빈을 선택하는 방법도 있습니다. 과제https://www.inflearn.com/blogs/6817 7~8일차이전까지는 JdbcTemplate을 활용해 직접 SQL 쿼리를 작성해 DB에 접근했습니다.이러한 경우 쿼리문에 실수를 해도 컴파일 시점에 오류를 파악할 수 없다는 문제점이 있습니다. 또한, SQL쿼리가 하나의 데이터베이스에 종속적이게 됩니다.그래서 JPA를 활용함으로써 이러한 단점들을 극복하려 했습니다.Jpa는 자바에서 데이터베이스에 접근할 수 있도록 도와주는 ORM입니다. 이 ORM을 통해 객체와 테이블을 매핑할 수 있는 기술을 제공해주고, 영속성 컨텍스트를 통해 한 트랜잭션 내 데이터를 영구히 저장할 수 있도록 해줍니다.영속성 컨텍스트의 특징은 다음과 같다.하나의 DB 작업 단위를 나타내는 트랜잭션 내부에서는 변경 감지 기능이 있다. 처음 DB에서 데이터를 조회하는 경우 데이터의 첫 상태를 스냅샷 형태로 저장한 뒤, 트랜잭션이 끝나는 시점에 데이터를 스냅샷과 비교하여 변경이 발생했다면 UPDATE쿼리를 통해 데이터를 자동 수정해준다.트랜잭션 내부에서 데이터를 삽입했다면 해당 트랜잭션이 끝날 때까지 데이터 삽입을 지연시킵니다.트랜잭션 내부에서 조회한 엔티티를 다시 조회할 경우 DB에 접근하지 않고 영속성 컨텍스트 내부에서 똑같은 인스턴스를 제공해준다. 과제https://www.inflearn.com/blogs/6850 9일차현재까지 배운 스프링 컨테이너, Jpa, 트랜잭션 이론을 활용할 수 있는 개발을 진행했습니다.Jpa를 스프링 내에서 좀 더 쉽게 사용할 수 있고 간단한 쿼리 메서드를 제공해주는 Spring Data Jpa를 활용해 개발을 진행했습니다.
백엔드
・
영속성컨텍스트
・
트랜잭션
・
스프링컨테이너
・
인프런워밍업클럽
2024. 02. 26.
0
인프런 워밍업 클럽 0기 - 백엔드 코스 (과제7)
문제 1. JPA 적용하기이전의 프로젝트 상에서 JdbcTemplate을 사용해 DB에 접근했었습니다. JdbcTemplate과 같은 경우 쿼리문을 string으로 작성하기 때문에 오류를 실제 동작 상황에서 파악할 수 있는 안 좋은 단점이 있었습니다. 이를 보완하기 위해 좀 더 객체와 테이블을 바로 매핑해서 코드로써 DB에 접근할 수 있는 JPA를 활용해보도록 하겠습니다. 그 중에서 JPA를 Spring에서 좀 더 간편하게 사용할 수 있는 Spring Data Jpa를 활용하고 Hibernate를 구현체로 사용해보도록 하겠습니다. Entity 생성@Entity public class Fruit { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private LocalDate warehousingDate; private long price; private boolean isSold; }Jpa에서 테이블과 매핑할 Entity를 생성해줍니다. Repository 생성public interface FruitJpaRepository extends JpaRepository { }Jpa를 활용할 Repository를 사용하는 방법은 JpaRepository를 상속받은 인터페이스를 선언해주는 것입니다. 이렇게 되면 JpaRepository에서 제공하는 간단한 메서드를 사용할 수 있고, 간단한 쿼리메서드를 작성해서 자신만의 쿼리를 작성할 수도 있습니다.저와 같은 경우 이전의 합계 계산 쿼리를 직접 작성해봤었습니다. 이전의 GROUP_BY를 활용한 코드를 이번에는 JPQL로 작성해보도록 하겠습니다. @Query(value = "SELECT new com.group.libraryapp.dto.fruit.FruitStatProjection(SUM(f.price),f.isSold) FROM Fruit f GROUP BY f.isSold") List findByIsSold();JPQL 또한 실제 Entity를 통해 쿼리를 작성할 수 있습니다. Fruit 엔티티의 isSold 필드를 통해 그루핑하고 각 그룹의 price 값의 총합을 구하는 쿼리를 작성해보았습니다. 해당 Repository를 활용한 Service는 이전과 거의 동일한 형태이므로 작성하지 않도록 하겠습니다. 문제 2. count queryDB에 해당 이름을 가진 과일 개수를 세는 쿼리를 작성해보도록 하겠습니다.Jpa는 count로 시작하는 쿼리 메서드를 COUNT 쿼리가 나가도록 정해두었습니다. 그리고 이름을 통해 필터링할 것이기 때문에 countByName(String name)으로 쿼리메서드를 지정하면 Spring Data Jpa가 이를 통해 실제 구현 코드를 스스로 작성해줍니다. public interface FruitJpaRepository extends JpaRepository { ... Long countByName(String name); }위와 같이 작성한다면 바로 사용할 수 있습니다.이를 사용하는 서비스와 컨트롤러 메서드는 별다른 점이 없으므로 따로 작성하지는 않겠습니다. 문제 3. option querypath: /api/v1/fruit/listquery: option, priceresponse{ "name": String, "warehousingDate: LocalDate, "price": long }option이 GTE일 경우 Greater Than Equal 연산을 수행하고 LTE일 경우 Less Than Equal 연산을 수행합니다. 우선 option과 price를 담아줄 DTO를 선언해주었습니다.public class FruitResponse { private String name; private LocalDate warehousingDate; private long price; }response가 FruitResponse의 list 형식으로 반환될 수 있도록 했습니다. @RequestParam과 같은 경우 option, price 두 인자를 따로 따로 받아야 함으로, @ModelAttribute을 활용해 두 쿼리파라미터를 한 번에 받아오도록 하겠습니다.FruitSearchpublic class FruitSearch { private String option; private long price; }FruitController @GetMapping("/api/v1/fruit/list") public List getFruitList(@ModelAttribute FruitSearch fruitSearch){ return fruitServiceV2.getFruitList(fruitSearch); }JpaRepositorypublic interface FruitJpaRepository extends JpaRepository { ... List findByPriceGreaterThanEqual(long price); List findByPriceLessThanEqual(long price); }쿼리 메서드를 두 개 선언해준 뒤 옵션에 따라 각각 실행될 수 있도록 해주었습니다. Service public List getFruitList(FruitSearch fruitSearch) { List result = fruitSearch.isGTE() ? fruitJpaRepository.findByPriceGreaterThanEqual(fruitSearch.getPrice()) : fruitJpaRepository.findByPriceLessThanEqual(fruitSearch.getPrice()); return result.stream().map(FruitResponse::new).collect(Collectors.toList()); } 응답 예시[ { "name": "사과", "warehousingDate": "2024-02-25", "price": 8000 }, { "name": "사과", "warehousingDate": "2024-02-25", "price": 3000 }, { "name": "배", "warehousingDate": "2024-02-25", "price": 4000 }, { "name": "귤", "warehousingDate": "2024-02-25", "price": 1000 }, { "name": "수박", "warehousingDate": "2024-02-25", "price": 10000 } ]
백엔드
・
springdatajpa
・
jpa
・
jpql
・
modelattribute
・
쿼리메서드
2024. 02. 25.
0
인프런 워밍업 클럽 0기 - 백엔드 코스 (과제6)
문제 1. Fruit 로직 계층형식으로 분리클린 코드에 대해 짤막하게 배우고 난 뒤, 의미 있는 네이밍과 메소드 분리의 중요성에 대해 알게 되었습니다.이전 과제4에서 수행했던 Fruit API 만들기에서는 Fruit과 관련된 로직을 모두 Controller에 작성하는 안 좋은?! 코드를 작성했었습니다. 이전의 코드는 다음과 같습니다. @PostMapping("/api/v1/fruit") public void createFruit(@RequestBody FruitCreateRequest request){ fruits.add(new Fruit(request)); } @PutMapping("/api/v1/fruit") public void sellFruit(@RequestBody Map request){ fruits.stream().forEach(fruit -> { if(request.get("id")==fruit.getId()){ fruit.sellFruit(); } }); } @GetMapping("/api/v1/fruit/stat") public FruitStatResponse getFruitStat(@RequestParam String name){ List filteredFruits = fruits.stream().filter(fruit -> fruit.getName().equals(name)).collect(Collectors.toList()); return new FruitStatResponse(filteredFruits); }보이는 것과 같이 모든 로직이 Controller 메서드에 포함되어 있습니다!현재는 아직 적은 양의 로직이지만, DB에 실제 접근하는 코드가 추가되고 다른 로직이 추가된다면 Controller 클래스의 크기는 굉장히 커지게 될 것입니다.그러므로 Controller, Service, Repository 계층으로 코드를 구분해 작성해주도록 하겠습니다.이처럼 계층 형식으로 구분해주는 이유는 하나의 메서드 상에서 모든 로직이 존재한다면 어떠한 곳에서 에러가 났는지 알기 어렵고 이 후 변경 사항이 생길 경우 수정에 어려움을 겪을 수 있기 때문입니다.그리고! 문제 2번 새로운 구성의 Repository가 생길 것을 대비해 Repository는 FruitRepository 인터페이스를 생성하고 이를 구현하는 식으로 변경해보도록 하겠습니다.각 계층 클래스는 스프링 빈으로 등록한 뒤, 스프링 컨테이너에 의해 주입 받아 사용하는 방식으로 구성하겟습니다. FruitController@RestController public class FruitController { private final FruitService fruitService; public FruitController(FruitService fruitService) { this.fruitService = fruitService; } @PostMapping("/api/v1/fruit") @ResponseStatus(HttpStatus.CREATED) public void createFruit(@RequestBody FruitCreateRequest request){ fruitService.registerFruit(request); } @PutMapping("/api/v1/fruit") public void sellFruit(@RequestBody Map request){ if(!request.containsKey("id")){ throw new IllegalArgumentException("해당 id의 과일이 존재하지 않습니다."); } fruitService.sellFruit(request.get("id")); } @GetMapping("/api/v1/fruit/stat") public FruitStatResponse getFruitStat(@RequestParam String name) { return fruitService.getFruitByName(name); } } FruitService@Service public class FruitService { private final FruitRepository fruitRepository; public FruitService(FruitRepository fruitRepository) { this.fruitRepository = fruitRepository; } public void registerFruit(FruitCreateRequest request) { fruitRepository.registerFruit(request); } public void sellFruit(Long fruitId) { fruitRepository.updateFruit(fruitId); } public FruitStatResponse getFruitByName(String fruitName) { return fruitRepository.findPriceInfoByName(fruitName); } } FruitRepositorypublic interface FruitRepository { void registerFruit(FruitCreateRequest request); void updateFruit(Long fruitId); FruitStatResponse findPriceInfoByName(String fruitName); boolean isFruitNotExist(long id); } FruitMemoryRepository@Repository public class FruitMemoryRepository implements FruitRepository { List fruits = new ArrayList(); @Override public void registerFruit(FruitCreateRequest request) { fruits.add(new FruitDTO(request)); } @Override public void updateFruit(Long fruitId) { fruits.stream().forEach(fruit -> { if (fruitId == fruit.getId()) { fruit.sellFruit(); } }); } @Override public FruitStatResponse findPriceInfoByName(String fruitName) { List filteredFruits = fruits.stream() .filter(fruit -> fruit.getName().equals(fruitName)) .collect(Collectors.toList()); return FruitStatResponse.createFruitStatMemory(filteredFruits); } @Override public boolean isFruitNotExist(long id) { return fruits.stream().anyMatch(fruit -> fruit.getId() == id); } } Fruit 도메인을 DB에서 관리하도록 변경하면서 id 필드가 없어지게 되었습니다. 그래서 id를 통해 Fruit값이 존재하는지 확인했던 MemoryRepository에 문제가 발생했습니다. 이를 해결하기 위해 Fruit을 전체로 상속받은 FruitDto를 추가해주었고 FruitMemoryRepository는 FruitDto 리스트를 가지도록 해주었습니다.이에 따라 여러 메서드들의 매개변수나 리턴 타입들을 수정해주었습니다. 문제 2. 새로운 Repository 형식 추가 & @Primary이전에는 어플리케이션 실행 시 Memory에 리스트 형식으로 Fruit을 저장하는 방식이었습니다. 이번에는 MySQL에 데이터를 영구히 저장하는 MySqlRepository를 추가해보도록 하겠습니다. FruitMySqlRepository@Repository @Primary public class FruitMySqlRepository implements FruitRepository { private final JdbcTemplate jdbcTemplate; public FruitMySqlRepository(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } private RowMapper rowMapper = BeanPropertyRowMapper.newInstance(FruitStatProjection.class); @Override public void registerFruit(FruitCreateRequest request) { String sql = "INSERT INTO fruit(name,warehousing_date,price,is_sold) VALUES(?,?,?,?)"; jdbcTemplate.update(sql, request.getName(), request.getWarehousingDate(), request.getPrice(), false); } @Override public void updateFruit(Long fruitId) { if(isFruitNotExist(fruitId)){ throw new IllegalArgumentException("해당 id에 일치하는 과일이 없습니다."); } String sql = "UPDATE fruit SET is_sold=true WHERE id = ?"; jdbcTemplate.update(sql,fruitId); } @Override public FruitStatResponse findPriceInfoByName(String fruitName) { String readSql = "SELECT SUM(price) as price,is_sold FROM fruit GROUP BY is_sold"; List result = jdbcTemplate.query(readSql, rowMapper); return FruitStatResponse.createFruitStatDB(result); } @Override public boolean isFruitNotExist(long id) { String readSql = "SELECT * FROM fruit WHERE id = ?"; return jdbcTemplate.query(readSql, (rs, rowNum) -> 0, id).isEmpty(); } }이전처럼 List에 Fruit을 저장하지 않고 JdbcTemplate을 이용해 DB에 데이터를 저장하고 조회하는 방식으로 구성하게 되었습니다.DB에 삽입, 수정하는 방법은 이전 배운 방식 그대로 활용했습니다.fruit stat을 반환하는 메서드와 같은 경우 모든 fruit을 조회하고 팔린 것과 팔리지 않은 Fruit을 분리하고 가격의 합을 구하는 로직을 또 구성하기 보다는 GROUP BY문을 이용해 DB 쿼리로 한 번에 조회할 수 있도록 해주었습니다. 또한, 이제 FruitRepository가 두 개 생기게 되었으므로 MemoryRepository 빈과 MySqlRepository 빈 중 스프링 컨테이너가 헷깔리지 않도록 @Primary로 주입할 빈을 알 수 있도록 지정해주었습니다.이외에도 @Qualifer를 활용해도 스프링 컨테이너가 지정한 하나의 빈만 자동으로 주입해주는 것을 확인할 수 있습니다.
백엔드
・
클린코드
・
계층형아키텍처
・
의존성주입
・
제어의역전
・
IOC
・
DependencyInjection
・
JdbcTemplate
・
group_by
・
row_mapper
2024. 02. 25.
0
인프런 워밍업 클럽 0기 - 백엔드 코스 (1주차 회고)
인프런 워밍업 클럽 0기 - 백엔드 코스의 한 주가 끝이 났습니다. 이번주 배운 내용들을 토대로 정리해보는 시간을 가져보도록 하겠습니다. 1일차1일차는 커리큘럼 상 1일차에 진행되는 강의 + 강의를 듣기 전의 setting과 기본 지식과 관련된 내용들이 포함되어 있었습니다. 프로젝트 진행에 필요한 IDE, Git, DB 등을 설치해주었습니다.Java를 사용한다면 알고 있을 JVM, JDK에 대한 설명부터 프로젝트 빌드툴들에 대해서도 알 수 있었습니다.1일차 실제 강의에서는 스프링 부트에 대한 설명과 HTTP, API 등 백엔드 개발에 필요한 지식들을 모두 습득할 수 있었습니다.그리고 GET 방식의 API를 설계하고 테스트해볼 수 있었습니다. @GetMapping, path variable 등에 대해 알 수 있었습니다. 과제https://beomseok37.tistory.com/181 2일차2일차에는 POST 방식의 API를 개발하고 테스트하는 시간을 가졌습니다.@RequestBody를 통해 전달하고 싶은 데이터를 json 형식으로 보낼 수 있게 되었습니다.그리고 지금까지 배운 내용들을 토대로 유저 생성, 조회 API를 개발해보았습니다. 과제https://www.inflearn.com/blogs/6553 3일차생성한 유저들을 현재까지는 어플리케이션이 동작할 때만 존재하는 배열 형식으로 저장해두었습니다.이를 영구적으로 저장할 수 있는 DB와 관련된 지식을 얻을 수 있었습니다.이번 프로젝트에서는 저장할 데이터들의 스키마가 존재하는 관계형 데이터베이스인 MySQL을 사용했습니다.MySQL 테이블 생성 관련 DDL과 데이터 조작 언어인 DML을 배울 수 있었습니다.또한, Spring Boot에서 데이터베이스를 사용할 수 있는 방법에 대해 배울 수 있었습니다. 과제https://www.inflearn.com/blogs/6633 4일차User에 대한 update, delete API를 작성해주었습니다.또한, 쿼리 실행 도중 발생할 수 있는 예외상항들을 따로 처리해줄 수 있게 했습니다. 과제https://www.inflearn.com/blogs/6640 5일차클린 코드에 대해 배울 수 있었습니다.코드도 한 언어로 쓰여진 글이기 때문에 가독성을 높이기 위해 항상 노력해야 됩니다.또한, 한 기능을 처리하기 위해 3000줄 자리의 코드를 한 메서드 상에 작성하면 유지보수에 어려움을 겪을 수 있습니다. 각각의 기능별로 메서드를 분리하여 한 메서드가 하나의 로직만 수행하도록 하는 것이 좋은 코드를 작성할 수 있는 방법입니다.클린 코드를 적용하기 위해, 계층마다 각각의 로직만 실행할 수 있도록 API 요청에 대한 응답을 내려주는 Controller, 실제 구현 로직을 수행하는 Service, DB에 접근하는 로직을 수행하는 Repository 계층으로 로직을 구분해주었습니다. 과제https://www.inflearn.com/blogs/6646
백엔드
・
인프런
・
인프런워밍업클럽
2024. 02. 21.
0
인프런 워밍업 클럽 0기 - 백엔드 코스 (과제 5)
문제. 클린 코드를 읽고 다음 코드를 수정해보기 우선 클린 코드라는 지식을 알아보기 위해 다음 블로그의 클린코드 핵심 요약을 살펴보았다.https://mangkyu.tistory.com/132 여기서 주목한 점은함수는 하나의 역할만 해야한다. 중복 제거유의미한 이름을 사용하라 위와 같이 세가지 입니다.이 부분을 명심하고 리팩토링을 해보도록 하겠습니다. 변수 타입 변경문제에서 제시된 r1, r2, r3 ... 등등 랜덤으로 나타난 숫자를 카운팅하기 위한 변수가 따로 존재하는 것은 굉장히 불편하다고 생각하여 배열로 표시해주었습니다.// 이전 int r1=0, r2=0, r3=0, r4=0, r5=0, r6=0; // 변경 int[] r = new int[6];아래 랜덤으로 얻어올 숫자를 반복되는 if, else if문으로 처리하지 않기 위해서도 배열로 변경하는 것이 좋을 것 같다고 생각했습니다. 메서드 분리 및 메서드 네이밍현재 main 함수에 모든 내용이 합쳐져 있습니다. 여기서 메서드로 떼어낼 수 있는 부분을 떼어내주었습니다.public static void main(String[] args) { int a = inputTryCount(); int[] r = getRandomNumberCount(a); printRandomNumberCount(r); }이와 같이 변경되면서 호출되는 메서드 명만 보더라도 메인 함수가 어떤 식으로 동작하는지 확인할 수 있습니다. 변수 네이밍변수 명이 a, r 등 이 변수가 무엇을 의미하는 것인지 알 수 없습니다.좋은 네이밍을 통해 해당 변수가 무엇을 의미하는지 빠르게 파악할 수 있도록 해주었습니다.public static void main(String[] args) { int tryCount = inputTryCount(); int[] randomNumberCount = getRandomNumberCount(tryCount); printRandomNumberCount(randomNumberCount); }a 변수는 사용자가 시도할 횟수를 나타내므로 tryCount로 변경해주었습니다.r 변수는 랜덤 숫자가 나타난 횟수를 저장하므로 randomNumberCount로 변경해주었습니다. 출력 부분 반복 제거출력 부분은 모든 출력 변수들을 한 줄씩 반복해서 처리해주고 있습니다.이와 같이 반복해서 처리해둔다면, 만약 새롭게 출력해야할 변수가 늘어난다면 출력을 위한 코드도 추가되게 되므로 안 좋은 경우라고 생각합니다.변수를 배열로 바꿔주면서 반복문을 활용할 수 있게 되었으므로, for문을 통해 코드 중복을 제거해주었습니다.for(int i=0;i 전체 코드public class Main { public static void main(String[] args) { int tryCount = inputTryCount(); int[] randomNumberCount = getRandomNumberCount(tryCount); printRandomNumberCount(randomNumberCount); } private static int inputTryCount(){ System.out.println("숫자를 입력하세요 : "); Scanner scanner = new Scanner(System.in); return scanner.nextInt(); } private static int[] getRandomNumberCount(int tryCount){ int[] randomNumberCount = new int[6]; for(int i=0;i
백엔드
・
클린코드
2024. 02. 21.
0
인프런 워밍업 클럽 0기 - 백엔드 코스 (과제 4)
문제 1. 새로운 과일 정보 생성 APImethod: POSTpath: /api/v1/fruitbody{ "name": "사과", "warehousingDate": "2024-02-21", "price": 1000 } 해결 방법Request Body DTO 생성public class FruitCreateRequest { private String name; private LocalDate warehousingDate; private Long price; public String getName() { return name; } public LocalDate getWarehousingDate() { return warehousingDate; } public long getPrice() { return price; } }Fruit 객체 생성public class Fruit { private Long id; private String name; private LocalDate warehousingDate; private long price; private boolean isSold; private static Long idCount = 1L; public Fruit(FruitCreateRequest request) { this.id = idCount++; this.name = request.getName(); this.warehousingDate = request.getWarehousingDate(); this.price = request.getPrice(); this.isSold = false; } public void sellFruit(Long id){ isSold=true; } 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; } }request body를 통해 얻어온 정보를 저장할 수 있도록 했습니다.isSold 맴버 변수로 해당 상품이 팔렸는지 알 수 있도록 했습니다. API 구성 @PostMapping("/api/v1/fruit") public void createFruit(@RequestBody FruitCreateRequest request){ fruits.add(new Fruit(request)); }fruitsCreateRequest를 통해 얻어온 과일정보를 통해 새로운 과일을 생성합니다.fruits에 새롭게 생성된 과일을 넣어줍니다. 문제 2. 과일 팔기method: PUTpath: /api/v1/fruitbody{ "id": 1 } 해결 방법API 구성 @PutMapping("/api/v1/fruit") public void sellFruit(@RequestBody Map request){ fruits.stream().forEach(fruit -> { if(request.get("id")==fruit.getId()){ fruit.sellFruit(); } }); }fruits 리스트를 stream().forEach()를 통해 해당 id의 과일에 팔렸다는 표시를 해줍니다. 문제 3. 과일의 팔린 가격과 아직 팔리지 않은 가격 조회method: GETpath: /api/v1/fruit/statparam: ?name={String} 해결 방법response로 보낼 DTO FruitStatResponse 생성public class FruitStatResponse { private long salesAmount; private long notSalesAmount; public FruitStatResponse(List filteredFruits) { this.salesAmount = 0; this.notSalesAmount = 0; filteredFruits.stream().forEach((fruit -> { if(fruit.isSold()){ salesAmount+=fruit.getPrice(); } else { notSalesAmount+=fruit.getPrice(); } })); } public long getSalesAmount() { return salesAmount; } public long getNotSalesAmount() { return notSalesAmount; } }생성자의 매개변수를 해당 과일 명의 과일만 담긴 리스트를 가져옵니다.해당 과일 중 팔린 것과 팔리지 않은 것의 가격의 총합을 구해줍니다.API 구성 @GetMapping("/api/v1/fruit/stat") public FruitStatResponse getFruitStat(@RequestParam String name){ List filteredFruits = fruits.stream().filter(fruit -> fruit.getName().equals(name)).collect(Collectors.toList()); return new FruitStatResponse(filteredFruits); }stream().filter()를 통해 해당 과일 이름의 과일 리스트를 추려냅니다.FruitStatResponse의 생성자에 필터링된 과일 리스트를 넘겨 가격 총합을 반환합니다.
백엔드
・
spring
・
dto
・
requestbody
・
requestparam
2024. 02. 21.
0
인프런 워밍업 클럽 0기 - 백엔드 코스 (과제 3)
익명 클래스와 람다식 람다식을 알아보기 위해서는 함수형 프로그래밍에 대해 알아보아야 한다. 함수형 프로그래밍은 프로그래밍의 패러다임입니다.이전의 절차지향 프로그래밍이나 객체지향 프로그래밍과 같이 생겨난 새로운 패러다임 중 하나입니다.함수형 프로그래밍은 자료 처리를 수학적 함수의 계산으로 취급하고 상태와 가변 데이터를 멀리하는 프로그래밍 패러다임의 하나이다. 프로그래밍에는 명령형 프로그래밍과 선언적 프로그래밍이 있다.명령형 프로그래밍 : 클래스에서 메서드를 정의하고, 필요할 때 그 메서드를 호출하는 것으로 명령하여 동작하는 방식선언적 프로그래밍 : 데이터가 입력으로 주어지고 데이터가 처리되는 과정을 정의하는 것으로 동작하는 방식함수형 프로그래밍은 선언적 프로그래밍이며, 람다가 지원되기 이전까지 자바는 완전한 명령형 프로그래밍이었다. 함수형 프로그래밍의 조건순수 함수 : 같은 입력이 들어오면 출력 또한 같다. Side Effect가 없다. 고차 함수 : 일급 함수의 특징을 만족해야 한다.함수의 인자로 함수를 전달할 수 있다.함수의 리턴값으로 함수를 사용할 수 있다.익명 함수 : 이름이 없는 함수(람다식)합성 함수 : 새로운 함수를 생성하거나 어떤 계산을 수행하기 위해 둘 이상의 함수를 결합하는 것(메서드 체이닝) 함수형 프로그래밍의 특징불변성: 함수 내에서 상태를 변경하지 않는다.참조 투명성: 프로그램 동작의 변경없이 관련 값을 대체할 수 있다일급 함수 : 함수를 함수의 매개변수로, 함수의 반환값으로, 변수나 자료구조에 담을 수 있다.게으른 평가 : 값이 필요한 시점에 평가한다.(람다식을 사용한 스트림에서 종단연산이 없다면 아직 값을 연산하지 않는다.) 람다식은 메서드를 하나의 식으로 표현하는 선언적 프로그래밍의 방법이다.이전에는 익명 클래스를 이용하여 익명 구현 객체를 사용할 수 있었다. 람다식의 특징메서드의 이름이 없다.어떠한 클래스에 종속되지 않은 함수이다.일급 시민이다. 람다식 사용법(int a1,int a2) -> {return a1+a2;} (int a1,int a2) -> a1+a2; (int a1) -> System.out.println(a1); a1 -> System.out.println(a1); 익명 클래스하나의 메서드만을 사용할 클래스라면 익명 클래스로 정의해 해당 메서드만 익명 클래스 내부에 선언하여 사용할 수 있도록 했다.public static void main(String[] args) { Comparator comp = new Comparator() { @Override public int compare(Integer o1, Integer o2) { return o1.compareTo(o2); } }; System.out.println(comp.compare(2, 1)); } 함수형 인터페이스함수형 인터페이스는 제너릭 타입을 타입 파라미터로 가진다. 인터페이스에 정의된 하나의 추상 메서드는 함수형 인터페이스의 구현체인 람다함수를 실행시킬 메서드가 된다.추상 메서드가 무조건 한개만 존재해야 한다.@FunctionalInterface 어노테이션을 통해 검증할 수 있다.java.util.function에 빌트인 함수형 인터페이스가 존재한다.필요한 경우 구현해서 사용할 수도 있다.함수형 인터페이스의 활용 방법 : https://bcp0109.tistory.com/313 람다식 vs 익명 클래스람다는 메서드로 생성된다. 익명 클래스는 새로운 클래스 파일이 생성된다.익명 클래스의 this는 해당 클래스를 가리키고, 람다의 this는 람다가 포함된 클래스를 가리킨다. 메소드 레퍼런스람다 표현식을 더 간단하게 표현하는 방법람다 표현식으로 표현할 경우Consumer func = text -> System.out.println(text); func.accept("Hello");메소드 레퍼런스로 표현할 경우Consumer func = System.out::println; func.accept("Hello");ClassName::MethodName 형식으로 입력한다.stream()에서 람다식을 활용하지 않고 메서드 레퍼런스로 표현식을 더 간단하게 해줄 수 있습니다. Referencehttps://alkhwa-113.tistory.com/entry/%EB%9E%8C%EB%8B%A4%EC%8B%9Dfeat-%EC%9D%B5%EB%AA%85-%EA%B5%AC%ED%98%84-%ED%81%B4%EB%9E%98%EC%8A%A4-vs-%EB%9E%8C%EB%8B%A4%EC%8B%9Dhttps://docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html
백엔드
・
익명클래스
・
람다식
・
함수형프로그래밍
・
함수형인터페이스
・
메서드레퍼런스
2024. 02. 19.
0
인프런 워밍업 클럽 0기 - 백엔드 코스 (과제 2)
문제 1. GET 방식의 API 만들기path : /api/v1/calc쿼리 파라미터 : num1, num2response{ "add": {덧셈결과}, "minus": {뺄셈결과}, "multiply": {곱셈결과} } 해결 방법@RequestParam 어노테이션 활용 방법 @GetMapping("/api/v1/calc") public CalculatorAllResponse calcByRequestParam(@RequestParam int num1, @RequestParam int num2){ return new CalculatorAllResponse(num1+num2,num1-num2,num1*num2); }dto 활용 방법@GetMapping("/api/v2/calc") public CalculatorAllResponse calcByDto(CalculatorAllRequest request){ return new CalculatorAllResponse(request.getNum1()+request.getNum2(),request.getNum1()-request.getNum2(),request.getNum1()*request.getNum2()); }결과 1결과 2 문제 2. 날짜를 입력하면 몇 요일인지 알려주는 GET API 만들기path : /api/v1/day-of-the-week쿼리 파라미터 : date(yyyy-mm-dd)response{ "dayOfTheWeek": "MON" } 해결 방법이 문제는 사실 모두 LocalDate 패키지를 활용하면 간편하게 해결 가능하다.@GetMapping("/api/v1/day-of-the-week") public Map getDayOfTheWeek(@RequestParam String date){ LocalDate today = LocalDate.parse(date, DateTimeFormatter.ISO_DATE); Map result = new HashMap(); result.put("dayOfTheWeek",today.getDayOfWeek().getDisplayName(TextStyle.SHORT, Locale.US)); return result; }쿼리 파라미터를 통해 받아온 오늘 날짜를 통해 today라는 LocalDate 인스턴스를 생성해준다.LocalDate.getDayOfWeek() 메서드를 통해 해당 날짜의 요일을 반환할 수 있다.실제로는 1~7 숫자가 반환되고 1 = Mon, 2 = TUE ... 등을 나타낸다.여기서 getDisplayName() 메서드를 통해 문제에서 요구한 String 형식으로 반환되도록 한다. 문제 3. 여러 수를 받아 총 합을 반환하는 POST API 만들기path : /api/v1/add-allbody{ "numbers": [1, 2, 3, 4, 5] }response: int 해결 방법@GetMapping("/api/v1/add-all") public int addAll(@RequestBody Map> request){ return request.get("numbers").stream().mapToInt(Integer::intValue).sum(); }body의 데이터는 @RequestBody 어노테이션을 통해 얻어옵니다.stream()을 통해 리스트 안에 모든 값들을 더해줄 수 있도록 합니다.
백엔드
・
인프런_워밍업_클럽_0기
・
api만들기
・
api
・
localdate
・
@RequestParam
・
@RequestBody
・
dto