박정은
수강평 작성수
1
평균평점
5.0
블로그
전체 6#카테고리
- 백엔드

2024. 02. 25.
0
인프런 워밍업 클럽 BE - 1주차 발자국
벌써 인프런 워밍업 클럽이 시작된지 일주일이 지났다. 기존에 이미 수료했던 강의였지만, 최근에는 Kotlin을 주로 사용하면서 자바 언어에 대한 어색함이 있어서 이번 기회에 다시 수강하면서 자바에 조금 더 익숙해지고, 스프링에 대해서 더 알아가고 공부하고 싶어서 신청한 스터디였다. 1일차'서버 개발을 위한 환경 설정 및 네트워크 기초'에 해당하는 1~5강을 아주 간단하게 훑었다. 이 부분은 기본적인 내용이고 기존에 수강했었기에 강의를 거의 넘기듯이 봤다. 2강. '@SpringBootAppication과 서버' 강의에서 스프링의 어노테이션에 대해 간단히 언급하셨는데, 이 때 까지는 '아 그냥 스프링에서 사용하는 문법이구나~' 정도로 생각하고 넘어갔다. 그런데 1일차 과제가 어노테이션에 대해서 블로그 글을 찾아보는 것이였다. 나는 그냥 찾아보고 정리하고 넘겨야지. 정도로 생각하고 있었는데, 디스코드를 보니 과제의 질문은 두 가지였지만, 사람들은 이에 대해서 열심히 알아보고, 딥하게 공부하며 질문하는 사람들을 보게 되었다. 이 때, 내가 지금까지 공부를 하면서 놓친 부분을 알게되었다. 나는 코딩이 그냥 돌아가면 되지의 마인드로 하고있었는데, 이건 잘못된 접근 방식이었던 것 같다. 다른사람들의 과제도 읽어보고, 질문도 읽어보고, 최태현님의 답변들도 보면서 무엇때문에 이러한 과제를 내주었는지, 왜 이런 기초적인 부분이 중요한지에 대해서 깨닫게 되어 열심히 저리하기 시작한 것 같다. 자극을 많이 받게 된 첫 날이었다. 2일차2일차는 GET API와 POST API를 만드는 과제였다. 6~9강을 들으며 API를 생성하는 기본적인 내용에 대해서 학습했다. DTO를 적용해서 과제를 진행할까 하다가 '강의에서 배운정도로만 적용을 하자'라는 생각으로 Controller에 모든 코드를 때려박는식으로 구현했다. 하지만, DTO를 구현하는 것이 보통의 상황에서 선택되는 것이기 때문에 강의대로만 맞추지 말고 더 나아가 해보는게 어땠을까 라는 아쉬움이 있었다. 그래도 이번 과제를 하면서 DTO와 생성자 개념에 대해서 더 자세히 알게 되었다. Body를 List 형태로 받아 POST API로 보내는 과제가 있었는데, 이 때 에러 코드를 보고 트러블슈팅을 하면서 기본 생성자의 필요성과 개념에 대해서 알게되어 유익한 시간이었다. 3일차 3일차에는 익명클래스와 람다식에 대해서 알아보는 시간이었다. 본격적으로 DB와 통신하는 API를 구현하기 전에 기초를 쌓는 시간이라고 생각했다. 이 날의 과제의 질문은 두개였지만, 키워드는 6개나 있었다. 나는 질문에 치중해서 알아봐서 익명 클래스, 람다 함수형 프로그램에 대해서만 공부하고 과제를 제출했는데, 나머지 키워드인 @FunctionalInterface, 스트림API, 메소드 레퍼런스에 대해서 알아보지 못한게 많이 아쉬웠다. 이에 대해서는 오늘 더 알아보기로! 4일차뭔가 본격적으로 API를 만들어보는 과제였다. 이를 위해 새로 프로젝트를 생성해서 mysql를 연동해보고 데이터베이스와 테이블을 생성하는 것 부터 시작했다. dependencies를 추가해주면서 gradle에 버전 명시를 하지 않았더니 mysql 라이브러리를 가져오지 못하는 에러가 있어서 잠시 혼란스러웠다. 하지만 버전을 명시해주면서 잘 해결된 걸 보고 다음부터는 문제가 있으면 버전부터 명시해주면 될 거라고 생각했다. 그리고 이 때 jdbcTemplate 쪽은 조금 어색해서 3번째 문제에서 어려움을 겪었는데, RowMapper도 써보고 queryForObject도 써보고 ResultSetExtractor도 써보면서 지식을 넓힐 수 있었던 유익했던 과제였다. 5일차이번 과제는 따로 API를 작성하는 것이 아니라 클린코드를 하는 과제였다. 제시된 과제는 변수명도 명확하지 않았고, 메인 클래스 하나에 기능별 구분 없이 한 번에 작성된 코드였다. 그래서 기능별로 함수를 먼저 분리를 하는게 우선이라고 생각했고, 변수명도 명확하게 바꿔주어서 이 함수와 변수가 무엇인지를 알 수 있도록 변경해주었다. 상수도 써보고, 클래스 단위로 나누기도 하였는데 여기서 더 좋은 코드로 바꾸려면 어떻게 해야할까? 라는 생각이 들었다. 하지만 나는 그저 여기서 마무리하고 과제를 제출했는데, 디스코드를 보니 사람들이 다른 사람들의 코드에 대해서 궁금해하기도 하고, 코드리뷰 할 사람들을 구하는 것을 보면서 나는 더 좋은 코드를 작성하는 방법에 대해서 조금 더 관심갖지 않았던게 아쉬웠다. 1주차 스터디를 마치며사실 가볍게 생각했던 스터디였다. 강제적인건 없고 자율적으로 이루어지는 스터디기에 시간이 되면 적당히 과제만 해야겠다. 라는 마인드였다. 하지만, 막상 1일차 과제를 진행하면서 사람들의 열정을 보니 나도 더 열심히 하고 싶어졌다. 그래서 약속이 있을 때도, 회식이 있을 때도, 너무 피곤할 때도 있었지만 사람들의 열정을 보며 마음을 고쳐먹고 과제를 진행했다. 그랬더니 매일매일 과제를 해나가는 내 모습이 뿌듯하기도 하고 실제로 생각하지 못했던 부분도 많이 생각하게 되고 공부하게 되면서 5일밖에 되지 않았지만 많은 것을 배운 것 같다. 과제를 할 때, 더 딥하게 알아갈 수도 있었지만 내가 너무 가볍게만 생각했던 것 같다. 다음주에는 우선 꾸준히 과제를 제출하고 열정을 배워가는게 다음주의 목표이고, 1주차보다 조금 더 딥하게 내용을 알아가 보도록 해야겠다.

2024. 02. 23.
0
인프런 워밍업 클럽 BE - 5일차 과제
진도표 5일차와 연결됩니다우리는 라는 개념을 배웠습니다. 에 대한 감각을 익히기 위해서는 어떤 코드가 좋은 코드이고, 어떤 코드가 좋지 않은 코드인지 이론적인 배경을 학습하는 것도 중요할 뿐 아니라, 다양한 코드를 읽어 보며 어떤 부분이 읽기 쉬웠는지, 어떤 부분이 읽기 어려웠는지, 읽기 어려운 부분은 어떻게 고치면 좋을지 경험해보는 과정이 필요합니다.이번 과제는 제시된 코드를 읽어보며, 코드를 더 좋은 코드로 고쳐나가는 과정입니다. 구글에 “클린 코드” 혹은 “클린 코드 정리”를 키워드로 검색해보면, 이론적인 배경을 충분히 찾아보실 수 있습니다. 🙂 그러한 내용들을 보며 제시된 코드를 더 좋은 코드로 바꿔보세요! (코드를 바꿀 때 왜 바뀐 코드가 더 좋은 코드인지 다른 사람에게 설명하신다고 생각해보시면 더욱 좋습니다.) [제시된 코드]여러 함수로 나누어도 좋습니다! 🙂여러 클래스로 나누어도 좋습니다! 🙂 우선, main 함수에 때려넣은 그대로 기능에 따라서 함수만 분리를 해보았다.public class Main { private static final int DICE_SIDES = 6; public static void main(String[] args) { System.out.print("숫자를 입력하세요 : "); Scanner scanner = new Scanner(System.in); int rolls = scanner.nextInt(); // 변수 명확하게 지정 int[] results = rollDice(rolls); printResults(results); } private static int[] rollDice(int rolls) { int[] results = new int[DICE_SIDES]; for (int i=0; i우선 메인 함수에서 주사위를 몇 번 받을지에 대한 변수로 rolls 를 설정해주면서, 변수명을 명확하게 변경해주었다.또한, 메인에서는 로직을 모두 삭제하고 입력 받기, 주사위 던지기, 결과 출력하기만 수행한다. 그리고 각 로직들은 함수로 분리하였다.int[] rollDice(int rolls) 함수에서는 입력한 숫자만큼 주사위를 돌리고, 그 결과를 results에 담는 함수이다.DICE_SIDES 라는 상수를 설정해서, 만약 주사위의 종류가 달라진다면 상수만 변경해주면 함수를 수정해 줄 필요가 없게 되었다.그리고 출력을 위해 printResults 함수를 분리시켰다. 이 또한, DICE_SIDES 크기만큼 출력하면 되므로 결과값을 돌면서 출력해주는 함수를 만들었다.근데 여기서 다시 한 번 거슬리는건 모든게 main 함수에 때려박아져 있는것이다. 이는 객체지향프로그래밍 관점과 맞지 않는다. 그렇기 때문에 객체지향적인 프로그래밍을 위해 이를 클래스 단위로 분리를 해보도록 하겠다.우선, 주사위와 관련된 프로그래밍이므로, Dice 라는 클래스를 생성하였다. 그리고 나머지 함수들과 결과는 모두 이 주사위와 관련된 함수들이므로 Dice 클래스는 이와 같이 작성하였다.import java.util.Scanner; public class Dice { private static final int SIDES = 12; private int roll() { return (int) (Math.random() * SIDES); } private int[] rollDice(int rolls) { int[] results = new int[SIDES]; for (int i=0; i Dice의 기본 정보를 정의하고, 여기서 관련 함수들을 작성하였다. 그리고 main 에서 사용할 함수는 startRollDice()로, 여기서 숫자를 입력받고 주사위를 굴리고 결과를 출력하는 것 까지 구현하여 기존의 main 함수에서 하는 기능을 수행한다.이렇게 작성한다면 main 함수는 아래와 같이 간소화될 수 있고, 객체지향적으로 프로그램이 작성되었다.public class Main { public static void main(String[] args) { Dice dice = new Dice(); dice.startRollDice(); } }
백엔드

2024. 02. 22.
0
인프런 워밍업 클럽 BE - 4일차 과제
진도표 4일차와 연결됩니다우리는 GET API와 POST API를 만드는 방법을 배웠습니다. 👍 추가적인 API 들을 만들어 보며 API 개발에 익숙해져 봅시다!우선 강의에 따라 h2 database를 쓰지 않고 mysql db와 연동하였다. 이를 위해 build.gradle 파일에 의존성을 추가하였다. dependencies { ... // 추가 runtimeOnly 'mysql:mysql-connector-java:8.0.32' }버전을 명시하지 않고 dependencies를 추가했더니 오류가 발생하였다. 그래서 Maven Repository에서 최신 버전을 찾아 명시하였더니 해결되었다. (원래 버전이 없어도 되는건줄 알았는데 스프링 버전의 차이에 따라서 다른걸까.. ?)Fruit Database 생성 및 테이블 생성-- mysql dependencies 추가 (버전 명시하지 않은 경우 오류) create database fruit; create table fruit( id bigint auto_increment, name varchar(45), warehousingDate timestamp, price bigint, primary key (id) )Request 및 Response를 위한 기본 FruitDto 생성테이블에 데이터를 저장하는 것은 응답값이 없으므로 response에서 DTO로 가져올 필요가 없어 getter만 추가하였다.// FruitDto package com.example.libraryapp.dto.fruit.request; import java.time.LocalDate; public class FruitDto { private String name; private LocalDate warehousingDate; private long price; public String getName() { return name; } public LocalDate getWarehousingDate() { return warehousingDate; } public long getPrice() { return price; } }FruitController 구현@RestController @RequestMapping("/api/v1") public class FruitController { private final JdbcTemplate jdbcTemplate; public FruitController(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } @PostMapping("/fruit") public void saveFruit(@RequestBody FruitDto request) { String sql = "insert into fruit (name, warehousingDate, price) values (?, ?, ?)"; jdbcTemplate.update(sql, request.getName(), request.getWarehousingDate(), request.getPrice()); } }테이블에 정보가 저장되는 것과 200 ok 응답을 확인할 수 있다.Int 대신 long을 사용한 이유는 무엇일까?int는 -2.1억~2.1억 까지의 정수값을 가질 수 있다 (-2^31~2^31-1)long은 -922경~922경 까지의 정수값을 가질 수 있다. (-2^63~2^63-1)id 같은 경우는, 행이 점점 늘어남에 따라 int 타입의 범위를 벗어나는 상황이 발생할 수 있으므로, 애플리케이션의 확장성을 고려하여 초기 단계부터 long 타입을 관행처럼 사용하고 있다. 또한, price와 같은 경우에도 더 큰 범위를 필요로 하는 상황이 발생할 수 있으므로 (ex. 50년 동안의 전체 판매 금액, 아파트 가격 등) 사용할 수 있는 값이 제한적인 int 타입을 사용하는 것 보다 long 타입을 사용하는 것이 더 적합할 수 있다.판매된 과일 정보를 관리하기 위해서 soldout 이라는 컬럼을 Boolean 타입으로 추가해준다.또한, 기본적으로는 판매되지 않았으므로 default값은 false로 주었다.alter table fruit add column soldout boolean not null default false;이후 Controller에 sellFruit 함수를 추가해준다. @PutMapping("/fruit") public void sellFruit(@RequestBody SellFruitRequest request) { String sql = "UPDATE fruit SET soldout = true WHERE id = ?"; jdbcTemplate.update(sql, request.getId()); }여기서, request parameter는 1개이긴 하지만 @RequestBody로 넘겨야하므로 sellFruit을 위한 DTO 클래스를 새로 생성하였다.// SellFruitRequest public class SellFruitRequest { private long id; public long getId() { return id; } }이제 과일 이름으로 조회하여 팔린 금액과 팔리지 않은 금액을 출력해야한다. 이를 위해 ResponseDto를 한 개 생성해줬다.public class FruitStatResponse { private long salesAmount = 0L; private long notSalesAmount = 0L; public FruitStatResponse(long salesAmount, long notSalesAmount) { this.salesAmount = salesAmount; this.notSalesAmount = notSalesAmount; } public FruitStatResponse() { } public long getSalesAmount() { return salesAmount; } public long getNotSalesAmount() { return notSalesAmount; } public void setSalesAmount(long salesAmount) { this.salesAmount = salesAmount; } public void setNotSalesAmount(long notSalesAmount) { this.notSalesAmount = notSalesAmount; } }단 하나도 팔리지 않았거나, 모두 팔린 경우를 대비하여 각각의 변수에 0L로 디폴트 값을 설정해주었다.이후, 컨트롤러 부분을 정의하였는데 단일 값을 출력할 것이므로 query 가 아닌 queryForObject 를 사용했다.queryForObject: sql의 결과가 단 하나인 경우에 사용한다. @GetMapping("/fruit/stat") public FruitStatResponse showFruitStat(@RequestParam String name) { String salesAmountSql = "SELECT SUM(price) AS sales_amount FROM fruit WHERE name = ? and soldout = true"; long salesAmount = jdbcTemplate.queryForObject(salesAmountSql, Long.class, name); String notSalesAmountSql = "SELECT SUM(price) AS sales_amount FROM fruit WHERE name = ? and soldout = false"; long notSalesAmount = jdbcTemplate.queryForObject(notSalesAmountSql, Long.class, name); return new FruitStatResponse(salesAmount, notSalesAmount); }이 경우, Long.class 로 리턴하기 때문에 null 값으로 인한 NullPointerException이 등장할 수 있는데, 이 경우 핸들링을 위해 try-catch 문으로 감싸서 데이터가 없는 경우를 핸들링 해주었다. @GetMapping("/fruit/stat") public FruitStatResponse showFruitStat(@RequestParam String name) { try { String salesAmountSql = "SELECT SUM(price) FROM fruit WHERE name = ? and soldout = true"; long salesAmount = jdbcTemplate.queryForObject(salesAmountSql, Long.class, name); String notSalesAmountSql = "SELECT SUM(price) FROM fruit WHERE name = ? and soldout = false"; long notSalesAmount = jdbcTemplate.queryForObject(notSalesAmountSql, Long.class, name); return new FruitStatResponse(salesAmount, notSalesAmount); } catch (Exception e) { throw e; } }3번을 다 풀고보니 이런 내용이 있었다.왜 sum은 적용하려고 하였으나 group by는 적용하려 하지 않았는가 ..잠시 뇌가 멈췄었나보다. 이제 group by로 다시 적용을 해보자. 그럼 query를 통해서 바로 적용이 가능할것 같다.강의에서는 정보를 조회해서 결과로 뿌릴때, List 와 같은 형태로 사용했으므로 jdbcTemplate.query 를 사용해서 RowMapper 익명클래스를 선언하고 mapRow 메서드를 오버라이드 해주었다.하지만, 이번 케이스에서는 query와 RowMapper를 사용할 수 없었다. 왜냐하면, RowMapper는 여러 행을 객체 리스트로 변환할 때 사용되는 클래스이기 때문이다. 이 경우, 리턴값이 List 형식이 아니라 단일한 객체 한 개 리턴이 필요하기 때문에, RowMapper가 아닌 ResultSetExtractor를 사용했다.그럼 여기서, ResultSetExtractor는 무엇일까?ResultSetExtractorquery 메소드를 ResultSetExtractor와 함께 사용할 때, 전체 ResultSet을 단일 객체로 추출할 때 사용된다.정확히 우리가 하려는 목적과 일치한다. query의 resultSet을 단일객체로 추출하려면 ResultSetExtractor를 사용하면 되는것이다.RowMapper: query의 resultSet을 객체 리스트로 변환ResultSetExtractor: query의 resultSet을 단일 객체로 변환ResultSetExtractor의 메서드는 extractData 이므로, 이를 이용하여 아래와 같이 구현하였다. @GetMapping("/fruit/stat") public FruitStatResponse showFruitStat(@RequestParam String name) { String sql = "SELECT soldout, SUM(price) as total FROM fruit WHERE name = ? GROUP BY soldout"; return jdbcTemplate.query(sql, new Object[]{name}, new ResultSetExtractor() { @Override public FruitStatResponse extractData(ResultSet rs) throws SQLException, DataAccessException { FruitStatResponse fruitStatResponse = new FruitStatResponse(); while (rs.next()) { boolean soldout = rs.getBoolean("soldout"); long total = rs.getLong("total"); if (soldout) { fruitStatResponse.setSalesAmount(total); } else { fruitStatResponse.setNotSalesAmount(total); } } return fruitStatResponse; } }); }그리고 이를 람다식으로 바꾸어 마무리 해주었다. @GetMapping("/fruit/stat") public FruitStatResponse showFruitStat(@RequestParam String name) { String sql = "SELECT soldout, SUM(price) as total FROM fruit WHERE name = ? GROUP BY soldout"; return jdbcTemplate.query(sql, new Object[]{name}, rs -> { FruitStatResponse fruitStatResponse = new FruitStatResponse(); while (rs.next()) { boolean soldout = rs.getBoolean("soldout"); long total = rs.getLong("total"); if (soldout) { fruitStatResponse.setSalesAmount(total); } else { fruitStatResponse.setNotSalesAmount(total); } } return fruitStatResponse; }); }강의에서 jdbcTemplate.query, 그리고 RowMapper을 배울때도 개념이 어렵다고 생각했는데, 이를 변형해서 단일객체로 리턴하기 위해 내용들을 찾아보면서 어려움을 많이 겪었다. 이번 과제를 통해 queryForObject와 query에 대해서 더 자세히 알게 되어서 아주 많이 배우게 된 과제였다.강의 출처: https://inf.run/XKQg)
백엔드

2024. 02. 21.
0
인프런 워밍업 클럽 BE - 3일차 과제
진도표 3일차와 연결됩니다우리는 JdbcTemplate을 사용하는 과정에서 익명 클래스와 람다식이라는 자바 문법을 사용했습니다. 익명 클래스는 자바의 초창기부터 있던 기능이고, 람다식은 자바 8에서 등장한 기능입니다. 다음 키워드를 사용해 몇 가지 블로그 글을 찾아보세요! 아래 질문을 생각하며 공부해보면 좋습니다! 😊 [키워드]익명 클래스 / 람다 / 함수형 프로그래밍 / @FunctionalInterface / 스트림 API / 메소드 레퍼런스 [질문]자바의 람다식은 왜 등장했을까?람다식과 익명 클래스는 어떤 관계가 있을까? - 람다식의 문법은 어떻게 될까? 익명 클래스(Anonymous Class)익명클래스는 단어 그대로 이름이 없는 클래스를 말한다. 이름이 없는 클래스이므로 기억되지 않아도 된다는 것이며, 나중에 다시 불러질 이유가 없다는 것이다. 프로그램에서 일시적으로 한 번만 사용되고 버려지는 객체라고 보면 된다. 즉, 일회용 클래스이다. 익명클래스는 클래스 정의와 동시에 객체를 생성할 수 있다. 따로 클래스 정의 없이 메소드 내에서 바로 클래스를 생성해 인스턴스화 할 수 있으며, 이렇게 클래스의 선언과 객체의 생성을 동시에 하기 때문에 단 한번만 사용될 수 있다. 익명클래스는 일회용으로 사용되고 버려지는 클래스이므로, 자식 클래스에서 자주 사용된다. 어떠한 메소드에서 부모 클래스의 자원을 상속받아 재정의하여 사용할 자식 클래스가 한 번만 사용되고 버려질 자료형이라면, 굳이 상단에 클래스를 정의하기보다는 지역 변수처럼 익명클래스로 정의하고 스택이 끝나면 삭제되도록 하는 것이 유지보수면에서도, 메모리면에서도 이점을 얻을 수 있는것이다. 재사용할 필요가 없는 일회성 클래스를 정의하고 생성하는 것은 비효율적이므로, 익명클래스를 사용하여 코드를 줄일 수 있다. 예를 들어, 부모 클래스와 자식 클래스가 다음과 같다고 해보자.// 부모 클래스 class Animal { public String bark() { return "동물이 운다"; } } // 자식 클래스 class Dog extends Animal { @Override public String bark() { return "멍멍"; } } public class Main { public static void main(String[] args) { Animal dog = new Dog(); dog.bark(); } } 위의 예시에서, 익명클래스는 클래스 정의와 동시에 객체를 생성할 수 있으므로 아래와 같이 익명클래스를 이용할 수 있다.// 부모 클래스 class Animal { public String bark() { return "동물이 운다"; } } public class Main { public static void main(String[] args) { Animal dog = new Animal() { @Override public String bark() { return "멍멍"; } }; // 익명클래스에는 끝에 반드시 세미콜론을 붙여줘야 한다. dog.bark(); } } 이렇게 익명클래스 방식으로 선언한다면 @Override 어노테이션을 통해 오버라이딩한 메소드만 사용이 가능하며, 새로 정의한 메소드는 익명클래스가 끝나는 순간(;) 사용이 불가능하다. 익명클래스는 클래스필드, 지역변수, 메소드 매개변수로 이용이 가능하다. 보통의 경우, 익명 클래스로 사용하는 일은 드물다. 하지만, 인터페이스를 익명 객체로 선언하여 사용할 때 진가가 발휘된다. 추상화 구조인 인터페이스를 일회용으로 구현하여 사용할 필요가 있을때, 익명클래스를 사용하면 아주 좋다. // 부모 클래스 interface IAnimal { public String bark(); public String run((); } public class Main { public static void main(String[] args) { // 인터페이스 익명 구현객체 생성 IAnimal dog = new IAnimal() { @Override public String bark() { return "멍멍"; } @Override public String run() { return "멍멍달려"; } }; // 익명클래스에는 끝에 반드시 세미콜론을 붙여줘야 한다. dog.bark(); dog.run(); } } 람다 표현식(Lambda Expression)람다 표현식이란, 함수형 프로그래밍을 구성하기 위한 함수식이며, 간단히 말해 자바의 메소드를 간결한 함수식으로 표현한 것이다. 지금까지 자바에서 메서드 표헌을 위해 클래스를 정의해야 했다면, 람다 표현식이 등장하면서 메서드의 이름과 반환값을 생략하고 이를 변수에 넣어 자바 코드를 매우 간결하게 만들어낼 수 있게 되었다. add 함수를 기존의 메서드 형식으로 표현한 것과, 람다식으로 표현한 방식을 비교해보자.// 기존 형식 int add(int a, int b) { return a + b; } // 람다식 (x, y) -> { return x + y; } // 함수에 리턴문 한줄만 있는 경우 중괄호와 return 키워드를 생략할 수 있다. (x, y) -> x + y 이와 같이, 메서드타입, 이름, 매개변수 타입, 중괄호, return 문을 생략하고 화살표 기호를 넣어 코드를 크게 줄였다. 이러한 표현식을 람다 표현식이라 하며, 이름이 없는 함수. 즉, 익명 함수(anonymous function) 라고도 한다. 람다 표현식은 왜 등장했을까?위의 내용을 보아, 람다 표현식이 등장한 이유는 불필요한 코드를 줄이고 가독성을 높이기 위함이다. 함수형 인터페이스의 인스턴스를 생성하여 함수를 변수처럼 사용하게 되며, 이러한 경우 메소드의 이름이 불필요하다고 여겨지고 컴파일러가 문맥을 살펴 타입을 추론하게 된다. 림다 표현식은 아래와 같은 장단점이 존재한다.람다 표현식의 장점코드를 간결하게 만들 수 있다.식에 개발자의 의도가 명확히 드러나기 때문에 가독성이 높아진다.함수를 만드는 과정 없이 한번에 처리할 수 있어 생산성이 높아진다.병렬프로그래밍이 용이하다. 람다 표현식의 단점익명함수는 재사용이 불가능하다.디버깅이 어렵다.중복코드가 생성되어 코드가 지저분해질 수 있다.재귀함수로는 부적합하다. 람다표현식은 이와 같은 장단점이 존재하므로, 상황에 따라 맞는 방법을 사용해야 한다. 람다식과 익명클래스의 관계익명클래스는 복잡하고 긴 자바 문법을 간결하게 하는 것에 초점을 둔다. 이러한 이유로 람다 표현식 문법과 매우 잘 어울리며, 실제로 함께 자주 쓰인다. 예를들어, 다음과 같은 익명 클래스에 람다 표현식을 적용한다면 코드를 아주 간결하게 작성할 수 있게 된다.Operate operate = new Operate() { public int operate(int a, int b) { return a + b; } }; // 람다식 사용 Operate operate = (a, b) -> { return a + b; }; // return만 존재하므로 아래와 같은 표현이 가능 Operate operate = (a, b) -> a + b; 위의 내용을 람다 표현식의 문법 위주로 요약해서 정리해보자면, 아래와 같다.// Usage (타입 매개변수, 타입 매개변수2, ...) -> { 실행문; ... } // example (int a) -> {System.out.println(a);} (a) -> { System.out.println(a); } a -> System.out.println(a) // 매개변수가 존재하지 않는 경우 () -> { 실행문; ... } 오늘은 익명클래스와 람다표현식에 대해 알아보았다.오늘 미처 알아보지 못한 함수형 프로그래밍 / @FunctionalInterface / 스트림 API / 메소드 레퍼런스에 대해서 더 자세히 알아볼 예정이다.참고https://mangkyu.tistory.com/113https://inpa.tistory.com/entry/JAVA-%E2%98%95-%EC%9D%B5%EB%AA%85-%ED%81%B4%EB%9E%98%EC%8A%A4Anonymous-Class-%EC%82%AC%EC%9A%A9%EB%B2%95-%EB%A7%88%EC%8A%A4%ED%84%B0%ED%95%98%EA%B8%B0https://inpa.tistory.com/entry/%E2%98%95-Lambda-Expression#%EB%9E%8C%EB%8B%A4%EC%8B%9D%EA%B3%BC_%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8_%EC%9D%B5%EB%AA%85_%ED%99%94%EC%82%B4%ED%91%9C_%ED%95%A8%EC%88%98
백엔드

2024. 02. 20.
0
인프런 워밍업 클럽 BE - 2일차 과제
진도표 2일차와 연결됩니다우리는 GET API와 POST API를 만드는 방법을 배웠습니다. 👍 추가적인 API 들을 만들어 보며 API 개발에 익숙해져 봅시다!첫번째 문제는 GET API를 만들어보는 것이다. API 명세가 아주 잘 되어있으니 아주 쉽게 구현이 가능할 것 같다.'5강. GET API 개발하고 테스트하기' 강의에 GET API를 만드는 방법을 아주 구체적으로 설명해주신다. 해당 강의를 바탕으로 소스코드를 작성해보았다.Contoller에 직접 계산 코드를 입력하여 만들었다. 뺄셈의 경우 큰 숫자에서 작은 숫자를 뺀 결과를 저장할 수 있도록 if, else 문으로 작성하였다. 그리고 결과는 따로 response dto class를 작성하지 않고 자바의 자료구조에 익숙해져볼 겸 Map 형식의 변수를 생성하여 해당 변수에 결과를 저장한 후 리턴하도록 작성하였다. 작성 코드@RestController public class CalculatorController { @GetMapping("/api/v1/calc") public Map calculate(CalculatorRequest request) { Map results = new HashMap(); results.put("add", request.getNum1() + request.getNum2()); if (request.getNum1() > request.getNum2()) { results.put("minus", request.getNum1() - request.getNum2()); } else { results.put("minus", request.getNum2() - request.getNum1()); } results.put("multiply", request.getNum1() * request.getNum2()); return results; }여기서, @RequestParam 으로 받지 않고 CalculatorRequest dto 클래스로 받았다. 그리고 Controller에 다 때려넣으니 괜히 코드가 조금 더러운 느낌이 든다. 조만간 좀 정리를 해봐야겠다.⁉ 여기서 궁금한 점이 있는데 보통 @RequestParam을 안쓰고 저렇게 DTO 클래스로 받는것도 일반적인걸까? 지난번 코드리뷰 때 @RequestParam을 쓰라고 해서 수정헀었는데 더 알아봐야겠다.Postman 테스트두 번째 문제 또한 GET API를 작성하는 것이다. 이번에는 로직이 조금 더 들어가는데 YYYY-MM-DD 형태로 date를 받으면 요일을 계산하여 리턴해주는 함수다. 이번에는 Map을 사용하지 않고 Response를 위한 DTO를 생성했다. DayOfWeekGetResponse DTO 작성public class DayOfWeekGetResponse { private final String dayOfTheWeek; public DayOfWeekGetResponse(String dayOfTheWeek) { this.dayOfTheWeek = dayOfTheWeek; } public String getDayOfTheWeek() { return dayOfTheWeek; } }String 형태의 dayOfTheWeek를 받아 저장하는 형태이다. 이를 위해 ctrl+n 으로 생성자와 getter를 자동 생성해주었다.calculateDays Controller 작성 @GetMapping("/api/v1/day-of-the-week") public DayOfWeekGetResponse calculateDays(@RequestParam LocalDate date) { String dayOfWeek = String.valueOf(date.getDayOfWeek()).substring(0, 3); // String dayOfWeek = date.getDayOfWeek().toString().substring(0, 3); return new DayOfWeekGetResponse(dayOfWeek); }@RequestParam 을 이용하여 LocalDate 형식의 date 변수를 받는다. 나는 사실 String 형태의 변수를 받아, 이를 LocalDate 형태로 반환해야하나 싶었는데, @RequestParam LocalDate date와 같이 변수를 받으면 date 쿼리 파라미터의 값이 LocalDate 형태로 자동 변환되어 저장된다고 한다. 즉, YYYY-MM-DD 형태로 값을 주어 LocalDate 자료형이 이해할 수 있도록 값을 전달해주면 자동으로 변환된다는 새로운 사실을 알게되었다.그리고 LocalDate 객체의 강력함도 알게되었다. 요일을 어떻게 계산해야하지? 라는 막연한 생각이 들었는데 LocalDate에서 다양한 함수를 제공해주고 있었고, 여기서 getDayOfWeek() 라는 메서드가 제공되고 있어 날짜를 받아 요일로 변환하는 기능을 아주 간단하게 처리할 수 있었다.여기서, 함수를 사용하려고 보니 getDayOfWeek의 반환형은 String이 아니라 DayOfWeek인 것을 확인할 수 있다.이를 확인해보니 enum 클래스로 구현되어 있었고, 모든 요일이 MONDAY, TUESDAY, ... 와 같이 열거되어 있었다.public enum DayOfWeek implements TemporalAccessor, TemporalAdjuster { /** * The singleton instance for the day-of-week of Monday. * This has the numeric value of {@code 1}. */ MONDAY, /** * The singleton instance for the day-of-week of Tuesday. * This has the numeric value of {@code 2}. */ TUESDAY, /** * The singleton instance for the day-of-week of Wednesday. * This has the numeric value of {@code 3}. */ WEDNESDAY, /** * The singleton instance for the day-of-week of Thursday. * This has the numeric value of {@code 4}. */ THURSDAY, /** * The singleton instance for the day-of-week of Friday. * This has the numeric value of {@code 5}. */ FRIDAY, /** * The singleton instance for the day-of-week of Saturday. * This has the numeric value of {@code 6}. */ SATURDAY, /** * The singleton instance for the day-of-week of Sunday. * This has the numeric value of {@code 7}. */ SUNDAY; 나는 DayOfWeek 타입을 그대로 사용하기보다는 String 형태로 변환하고, 예시로 나온 전체 글자를 출력하는 것이 아니라, substring을 이용하여 3글자만 출력되도록 하여 예제와 같이 결과값을 셋팅하였다. substring(0, 3)을 추가하지 않으면 아래와 같이 풀네임이 출력된다. substring(0,3) 적용 후 아래와 같이 변환되어 출력되는 것을 확인하였다. 예제를 보고 난 왜 "SUN"이 나오지?! 하고 흠칫헀는데 아무래도 2024-01-01 테스트 하신걸 잘 못 쓰셨나 보다. 헤헷 벌써 마지막 문제이다. 이번엔 POST API를 작성한다. 이번에는 @PostMapping 을 추가해주고, @RequestBody를 이용한다. 이를 위해 Request DTO를 작성해준다. CalculateNumbersRequest DTOpublic class CalculateNumbersRequest { private List numbers; public CalculateNumbersRequest() { } public CalculateNumbersRequest(List numbers) { this.numbers = numbers; } public List getNumbers() { return numbers; } } 이번에는 다른 것과 다르게 파라미터와 본문이 아무것도 없는 기본생성자를 추가해주었는데, 이 기본생성자의 추가 없이는 에러가 발생했기 때문이다. 기본생성자를 추가해야하는 이유JSON이나 XML 같은 외부 데이터 소스로부터 객체를 역직렬화할 때, 객체를 인스턴스화 하기 위해 기본 생성자를 사용한다. 이를 추가하지 않았을 때 났던 에러 메시지는 다음과 같다(no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)이러한 오류를 보았을 때, 역직렬화하는 과정에서 발생한 문제로 생각된다. 이와 같이 DTO를 추가한 후 Controller를 아래와 같이 작성해주었더니 제대로 동작한다. @PostMapping("/api/v1/calc-numbers") public int calculateNumbers(@RequestBody CalculateNumbersRequest request) { int sum = 0; for(int number: request.getNumbers()) { sum += number; } return sum; } List 형태로 numbers를 받았으므로, request.getNumbers()를 통해 모든 값들을 더한 결과를 반환하도록 하였다. 포스트맨으로 결과를 확인하니 다음과 같이 잘 동작하는 것을 확인하였다. +추가과제가 끝나고 안읽었던 디스코드 스레드를 읽어보면서 lombok의 annotation에 대해 접하게 되었다.그래서 문제3번을 풀면서 작성했던 DTO 클래스를 lombok으로 구현해봤다. 우선 build.gradle파일에 lombok 의존성을 주입해주었다dependencies { ... compileOnly 'org.projectlombok:lombok:1.18.30' annotationProcessor 'org.projectlombok:lombok:1.18.30' } 의존성을 주입하니 lombok의 어노테이션인 @Getter 와 @NoArgsConstructor 등이 인식되었다.@Getter @NoArgsConstructor public class CalculateNumbersRequest { private List numbers; } 이렇게 DTO 클래스를 작성하니 Controller 파일은 수정을 안해도 문제없이 돌아가는걸 확인했다. 덕분에 새로운걸 많이 알게되고 기능 구현에 급급해서 자세히 깊게 알아갈 생각조차 못해봤는데, 이렇게 다함께 고민하는 자리에 있으니 많은 것을 알게되어 새롭고 기쁘다. 아직 궁금한게 많지만 차근차근 공부하면서 더 알아가보는걸로 ~!

2024. 02. 19.
1
인프런 워밍업 과제 BE - 1일차 과제
어노테이션을 사용하는 이유(효과)는 무엇일까?어노테이션이란?어노테이션은 프로그램 내에서 주석과 유사하게, 프로그래밍 언어에는 영향을 미치지 않으면서 프로그램에 유의미한 정보를 제공하는 역할을 한다. 이 속성을 어떤 용도로 사용할지, 이 클래스에 어떤 역할을 부여할지 결정하게 된다.혹은, 인터페이스 기반으로 한 문법으로 주석처럼 코드에 달아 클래스에 특별한 의미를 부여하거나 기능을 주입할 수 있다. 어노테이션을 사용하는 효과로직 흐름에 대한 컨텍스트가 응축되어 있어 불필요한 반복코드가 줄고 개발자가 비지니스 로직에 더 집중할 수 있도록 해준다.설정 정보를 코드에 포함시켜 유지할 수 있으며 설정 파일을 공유하는 불편함이 없다.소스코드의 로직을 방해하지 않고, 특정 프로그램을 위해 정보를 제공할 수 있다. 나만의 어노테이션은 어떻게 만들 수 있을까?커스텀 어노테이션은 왜 사용할까?역시 어노테이션이 가진 가장 큰 장점은 간결함이다. 커스텀 어노테이션을 사용하는 방법어느 한 블로그에서는 커스텀 어노테이션 사용과 실행절차를 사과가 포장되는 과정에 비유하여 설명하였다.판매가능한 사과에는 판매가능(@CanSale) 라벨지를 달아서 포장과정시 이를 판별하여 제품을 포장한다고 가정한다면, 타겟에 적합한 라벨지를 만들어야 한다. 라벨에는 이 라벨을 붙일 대상을 명시하고, 언제까지 타겟에 부착할 것인지를 적는다. 그리고 라벨에 이름을 적으면 라벨이 만들어진다. 사과의 상태에 따라 판매가능 라벨을 부착하여 싱싱한 사과라는 타겟에 판매가능이라는 어노테이션을 부착하게 되고, 제품 포장 과정에서는 이 라벨을 보고 판별하여 제품을 포장하는 것이다.위의 비유와 같이, 어노테이션을 정의한 후 원하는 타겟에 적용하면 된다. 그 후, 어노테이션이 붙은 타겟을 어떻게 사용할지 기능을 구현하면 해당 기능이 실행될 때 타겟에 붙은 어노테이션에 따라서 타겟의 경로가 결정된다.위의 시나리오로 CanSale 어노테이션을 정의해보자.CanSale 어노테이션 정의하기@Target(ElementType.TYPE) // 클래스 레벨에 어노테이션 적용 @Retention(RetentionPolicy.RUNTIME) // 런타임까지 어노테이션 정보 유지 public @interface CanSale { String target(); String validUntil(); String labelName(); }@Target: 선언된 어노테이션이 적용될 수 있는 Java 요소의 종류를 지정한다. 클래스, 메소드, 필드, 파라미터 등이 해당한다. 이를 통해 어노테이션의 적용 범위를 제한하여 개발자가 의도하지 않은 곳에 어노테이션이 사용되는 것을 방지한다.@Retention: 어노테이션의 정보가 언제까지 유지될 것인지를 지정한다.RetentionPolicyu.SOURCE : 소스파일에만 유지되며, 컴파일러에 의해 제거된다.RetentionPolicy.CLASS : 바이트코드 파일에는 존재하지만, 런타임에는 사용할 수 없다.RetentionPolicy.RUNTIME : 런타임에도 유지되어 리플렉션을 통해 어노테이션 정보를 조회할 수 있다.@Interface : 커스텀 어노테이션을 정의할 때 사용하는 키워드이다. 이 어노테이션 내부에는 여러 요소(element)를 정의할 수 있고, 각 요소는 어노테이션을 적용할 때 설정할 수 있는 속성을 의미한다.CanSale 어노테이션을 사용하는 클래스 정의하기@CanSale(target = "FreshApple", validUntil = "2024-02-19", labelName = "FreshAppleSale") public class Apple { private boolean isFresh; public Apple(boolean isFresh) { this.isFresh = isFresh; } public boolean isFresh() { return isFresh; } }@CanSale : 커스텀으로 정의한 어노테이션이다. 판매가능한 사과에 대한 정보를 담기위해 생성하였으며, target, validUntil, labelName과 같은 요소들을 포함하여 판매대상, 유효기간, 라벨이름을 지정하는데 사용되었다.어노테이션 정보 처리하기public class PackagingProcess { public static void packageProduct(Object product) throws Exception { if (product.getClass().isAnnotationPresent(CanSale.class)) { CanSale saleInfo = product.getClass().getAnnotation(CanSale.class); System.out.println("Packaging " + saleInfo.target() + ". Valid until: " + saleInfo.validUntil() + ". Label: " + saleInfo.labelName()); // 유효기간, 라벨이름 등을 판단하여 포장 처리 } else { System.out.println("판매할 수 없는 물건입니다."); } } public static void main(String[] args) throws Exception { Apple apple = new Apple(true); // 싱싱한 사과 인스턴스 생성 packageProduct(apple); // 제품 포장 시도 } }제품포장 과정에서 CanSale 어노테이션이 있ㄴ는지 확인하고 그 정보를 바탕으로 판매 가능한 사과를 포장하는 로직을 구현한다. 커스텀 어노테이션을 선언할 때 메타 어노테이션을 함께 사용하기메타 어노테이션(meta-annotation) 이란?어노테이션을 정의할 때 사용되는 어노테이션이다. 즉, 다른 어노테이션을 위한 어노테이션으로, 어노테이션의 동작 방식을 지정하는데 사용된다. 주요 메타 어노테이션@Target어노테이션이 적용될 수 있는 요소를 지정한다. 어노테이션의 사용 범위를 명확히하여 어노테이션의 적용 오류를 방지한다.@Retention정보가 유지되는 시점을 지정한다. 세가지 RetentionPolicy가 존재한다.@Documented어노테이션 정보가 JavaDoc 문서에 포함될지 여부를 지정한다. 이를 사용하면 어노테이션이 적용된 요소의 문서에도 어노테이션 정보가 나타난다.@Inherited어노테이션이 하위 클래스로 상속될지 여부를 지정한다. 클래스에만 적용되며, 어노테이션이 적용ㄷ된 클래스를 상속받는 하위 클래스에서도 해당 어노테이션 정보를 유지하게 된다.@Repeatable같은 어노테이션을 하나의 요소에 여러번 적용할 수 있게 한다. 어노테이션을 담을 컨테이너 어노테이션을 함께 정의해야 한다. 참고https://seongeun-it.tistory.com/142https://www.nextree.co.kr/p5864/https://ittrue.tistory.com/158
백엔드




