블로그
전체 32024. 02. 21.
0
자바의 람다식에 대하여
[자바의 람다식 등장 배경]자바의 람다식은 함수형 프로그래밍의 개념을 자바에 도입하기 위해 등장했습니다. 함수형 프로그래밍은 함수를 일급 객체로 취급하여 함수를 변수에 할당하고, 다른 함수의 매개변수로 전달하거나 함수의 반환값으로 사용하는 등의 작업을 허용하는 프로그래밍 패러다임입니다. 이는 코드의 간결성과 가독성을 향상시키고, 병렬 처리와 동시성을 더욱 쉽게 다룰 수 있는 장점을 제공합니다.[람다식과 익명 클래스의 관계]람다식은 익명 클래스를 간결하게 표현하는 방법 중 하나입니다. 익명 클래스는 인터페이스를 구현하거나 추상 클래스를 상속받는 클래스를 정의할 때 사용되는데, 이때 클래스의 인스턴스를 생성하면서 동시에 메소드를 정의할 수 있습니다. 람다식은 이러한 익명 클래스의 특성을 활용하여 코드를 더 간결하게 표현합니다.[람다식의 문법]람다식의 기본 문법은 다음과 같습니다:(매개변수 목록) -> { 실행 코드 } 예를 들어, 정렬 기준으로 사용될 Comparator를 구현하는 익명 클래스를 람다식으로 표현하면 다음과 같습니다:Comparator comparator = (String s1, String s2) -> { return s1.compareTo(s2); }; 람다식은 매개변수 목록과 실행 코드로 이루어져 있으며, 실행 코드가 단일 표현식이라면 중괄호({})를 생략할 수 있습니다. 또한, 매개변수의 타입을 추론할 수 있는 경우에는 매개변수의 타입을 생략할 수도 있습니다. 위의 예제를 간결하게 표현하면 다음과 같습니다:Comparator comparator = (s1, s2) -> s1.compareTo(s2); 이처럼 람다식을 사용하면 코드가 훨씬 간결해지고 가독성이 향상되며, 함수형 프로그래밍의 장점을 자바에서도 활용할 수 있게 됩니다.[키워드 재정리]일급 객체:변수나 데이터 구조 안에 담을 수 있어야 합니다.매개변수로 전달할 수 있어야 합니다.반환값으로 사용할 수 있어야 합니다.할당에 사용된 이름과 관계없이 고유한 구별이 가능해야 합니다.익명 클래스:클래스의 정의와 인스턴스 생성을 동시에 할 수 있습니다.클래스 이름이 없으므로 한 번만 사용되거나 클래스의 이름이 필요하지 않은 간단한 작업에 유용합니다.주로 인터페이스의 메소드를 오버라이딩하여 구현합니다.@FunctionalInterface
2024. 02. 19.
1
인프런 워밍업 클럽0기 BE 2일차 - GET, POST API 만들어보기
덧셈, 뺄셈, 곱하기 계산기 만들기method: GETpath: /api/v1/calcqueryParams: num1(int), num2(int) Req 클래스public class CalculatorReq { private int num1; private int num2; public CalculatorReq(int num1, int num2) { this.num1 = num1; this.num2 = num2; } public int getNum1() { return num1; } public int getNum2() { return num2; } } Res 클래스 public class CalculateResponse { private int add; private int minus; private int multiply; public CalculateResponse(int add, int minus, int multiply) { this.add = add; this.minus = minus; this.multiply = multiply; } public int getAdd() { return add; } public int getMultiply() { return multiply; } public int getMinus() { return minus; } } Controller 소스코드 @GetMapping("/api/v1/calc") public CalculateResponse calculateTwoNumbers(CalculatorReq req) { int add = req.getNum1() + req.getNum2(); int minus = req.getNum1() - req.getNum2(); int multiply = req.getNum1() * req.getNum2(); return new CalculateResponse(add, minus, multiply); }dto 에서 각 값을 저장하게 만드는 것도 가능하겠지만, dto 에 로직이 들어가게 되어 유지보수성이 좋지 않을거라 판단하였습니다.주요 로직일 경우 서비스 계층을 따로 두어 처리하는 것이 맞겠지만, 현재의 요구사항은 컨트롤러 상에서 처리하는 것도 복잡도를 증가시키지 않을거라 생각하였습니다. 실행결과 무슨요일인지 알려주는 API 만들기 Method: GETPath: /api/v1/day-of-the-weekParam: Date(String) Res 클래스public class DayOfWeekResponse { String dayOfTheWeek; public DayOfWeekResponse(String dayOfTheWeek) { this.dayOfTheWeek = dayOfTheWeek; } public String getDayOfTheWeek() { return dayOfTheWeek; } } Controller@GetMapping("/api/v1/day-of-the-week") public DayOfWeekResponse dayOfTheWeek(@RequestParam String date) { LocalDate localDate = LocalDate.parse(date); DayOfWeek dayOfWeek = localDate.getDayOfWeek(); String displayName = dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.ENGLISH).toUpperCase(); return new DayOfWeekResponse(displayName); } 이름의 형식을 바꿔주는 책임을 누가 가져야하는지 고민을 많이 했지만, 표현계층인 컨트롤러 계층에서 그 역할을 가져가는 것이 맞다 생각하여 위와 같이 작성하였습니다. 실행결과예시의 날짜를 넣었을 때, SUN 이 출력되어 뭔가 잘못되었나 싶었지만, 달력을 열어보니 해당일은 일요일이 맞았습니다. 숫자의 합 반환하는 API 만들기 Method: POSTPath: /api/v1/sum-of-the-numbersParam: numbers(array) Req 클래스package com.group.libraryapp.dto.calculator.request; import java.util.ArrayList; import java.util.List; public class SumOfTheNumbersReq { private List numbers = new ArrayList(); public SumOfTheNumbersReq() { } public SumOfTheNumbersReq(List numbers) { this.numbers = numbers; } public List getNumbers() { return numbers; } } 기본 생성자가 없을경우 에러가 발생한다. Jackson 에서 발생한 문제로 역직렬화 관련 에러 메시지가 발생하였다. 그런데..! 나머지 얘기는 고찰에서 이어서 하겠습니다. Controller 소스코드 @PostMapping("/api/v1/sum-of-the-numbers") public int sumOfTheNumbers(@RequestBody SumOfTheNumbersReq req) { return req.getNumbers().stream() .reduce(Integer::sum) .get(); } 실행결과 음... 근데 이상하다. 위에서 사용한 CaculatorReq 클래스를 보게 되면 역시 기본 생성자를 가지고 있지 않다.(기본 생성자를 꼭 필요로 한다면 보통 리플렉션과 관련된 개념인데, 요청값에 대한 mapping 이 reflection 을 사용하지 않는다고 알고 있어서 의아했다.)SumOfTheReq 클래스와 CaculatorReq 의 차이점을 생각해보기로 했다.(손이 익숙하게 기본생성자를 만들거나, @NoArgs... 를 만들지 않았다 물론 lombok 이 없어서일수도 있지만)자바에서의 타입은 기본형과 참조형으로 나뉜다. 기본형으로만 이루어지면 괜찮은걸까?Field의 갯수의 문제일까?실험 1.1개의 기본형만을 가지는 Dto package com.group.libraryapp.dto.calculator.request; public class Temp { private int number; public Temp(int number) { this.number = number; } public int getNumber() { return number; } } @PostMapping("/api/v1/temp") public int temp(@RequestBody Temp temp) { return temp.getNumber(); } 실험 2.2개의 참조형을 갖는 Dtopackage com.group.libraryapp.dto.calculator.request; import java.util.ArrayList; import java.util.List; public class SumOfTheNumbersReq { private List numbers = new ArrayList(); private List tests = new ArrayList(); // public SumOfTheNumbersReq() { // } public SumOfTheNumbersReq(List numbers, Listtests) { this.numbers = numbers; this.tests = tests; } public List getNumbers() { return numbers; } public List getTests() { return tests; } } @PostMapping("/api/v1/sum-of-the-numbers") public int sumOfTheNumbers(@RequestBody SumOfTheNumbersReq req) { return req.getNumbers().stream() .reduce(Integer::sum) .get(); } 음... Jackson의 문제가 맞았다는걸 알 수 있었다. 필드값이 하나인 상황에서 기본생성자가 없을 경우 Jackson의 에러를 키워드로 검색해보니 https://github.com/FasterXML/jackson-databind/issues/3085해당 글을 발견할 수 있었다 답변을 발췌하면This is a well-known issue due to ambiguity of the 1-arg constructor case: single-argument could match either:Delegating case like "string-value" ORProperties-based case like {"name" : "value"}and if user does not specify mode with@JsonCreator(mode = JsonCreator.Mode.PROPERTIES) // or Mode.DELEGATING Jackson will try to guess which one to use. In your case it likely guesses that DELEGATING mode is to be used and expects a String, not Object value.라고 한다. 어떤 방식으로 역직렬화를 할지 몰라서 벌어지는 일이라고 하니 알아두면 좋을 것 같다.@JsonCreator 로 해결 가능하다는 답변도 함께 제시주었다.(감사합니다)@JsonCreator 를 통해 확인해보도록 하자package com.group.libraryapp.dto.calculator.request; import com.fasterxml.jackson.annotation.JsonCreator; import java.util.ArrayList; import java.util.List; public class SumOfTheNumbersReq { private List numbers = new ArrayList(); @JsonCreator public SumOfTheNumbersReq(List numbers) { this.numbers = numbers; } public List getNumbers() { return numbers; } } 잘 동작하는 모습을 볼 수 있었다. 개인적으로는 특정 클래스를 만들 때에 필드가 하나인 경우가 드물기 때문에 자주보기는 힘든 문제라고 생각하지만, 어떤 경우가 벌어질 지 모르니 알아두자!
2024. 02. 17.
0
자바의 Annotation 에 대해서
어노테이션을 사용하는 이유 (효과) 는 무엇일까? 나만의 어노테이션은 어떻게 만들 수 있을까?어노테이션을 사용하는 이유와 효과어노테이션을 이용하여 얻을 수 있는 이점은 다음과 같습니다.코드 문서화와 가독성 향상: 어노테이션은 코드에 메타데이터를 포함하여 읽기 쉽고 이해하기 쉽도록 도와줍니다. 예를 들어, @Override 어노테이션은 해당 메서드가 부모 클래스나 인터페이스의 메서드를 재정의한다는 것을 명시적으로 나타냅니다. 이는 코드를 읽는 사람들에게 재정의된 메서드라는 사실을 명확히 전달해줍니다.컴파일 타임 체크와 안전성 보장: 어노테이션을 사용하여 컴파일 타임에 코드를 검사하고 오류를 방지할 수 있습니다. 예를 들어, @Override 어노테이션은 해당 메서드가 부모 클래스나 인터페이스의 메서드를 재정의하는지 여부를 컴파일러가 검사할 수 있도록 도와줍니다. 이는 잘못된 메서드 시그니처로 인한 오류를 사전에 방지할 수 있습니다.런타임 처리와 동적 기능 추가: 어노테이션은 런타임에 클래스나 메서드의 동작을 변경하거나 보완하는 데 사용할 수 있습니다. 예를 들어, 스프링 프레임워크에서 @Autowired 어노테이션은 의존성 주입을 자동화하는 데 사용됩니다. 또한 JUnit 프레임워크에서 @Test 어노테이션은 해당 메서드가 테스트 메서드임을 나타내어 테스트 실행 중에 인식될 수 있도록 도와줍니다.커스텀 도메인과 메타프로그래밍: 개발자는 자신만의 어노테이션을 정의하여 도메인 특정 기능을 확장할 수 있습니다. 이를 통해 메타프로그래밍 기법을 사용하여 더 유연하고 효율적인 코드를 작성할 수 있습니다. 나만의 어노테이션은 어떻게 만들 수 있을까?대상타입 (@Target), 유효시간(@Retention) 을 설정한 후 해당 어노테이션이 설정된 경우에 수행할 작업을 작성해주면 된다. 예를 들어 문자열에 FU 가 포함되어 있는 경우 이를 공백문자열로 바꾸는 어노테이션을 만들어보자. 코드로 보면 다음과 같습니다.어노테이션 정의import java.lang.annotation.*; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface ReplaceFU { }특정 문자열(Field)을 대상으로 하기에 @Target은 ElementType.FIELD문자열의 검사는 런타임시에 이뤄져야 하기 때문에 @Retention(RetentionPolicy.RUNTIME) 을 적용하여 만들었습니다. 어노테이션 처리import java.lang.reflect.Field; public class AnnotationProcessor { public static void process(Object obj) throws IllegalAccessException { Class clazz = obj.getClass(); Field[] fields = clazz.getDeclaredFields(); for (Field field : fields) { if (field.isAnnotationPresent(ReplaceFU.class)) { field.setAccessible(true); String value = (String) field.get(obj); if (value != null && value.contains("FU")) { field.set(obj, value.replace("FU", "")); } } } } } 런타임에 리플렉션을 활용하여 값을 바꿔주기 위해 위와 같이 작성하였습니다. 필드에 어노테이션 적용 및 테스트public class MyClass { @ReplaceFU private String myField; public MyClass(String myField) { this.myField = myField; } } public class Main { public static void main(String[] args) throws IllegalAccessException { MyClass myObject = new MyClass("This is FU example."); System.out.println("Before processing: " + myObject.getMyField()); AnnotationProcessor.process(myObject); System.out.println("After processing: " + myObject.getMyField()); } }Before processing: This is FU example. After processing: 어노테이션의 처리의 경우 IDE 상에서 추적하기가 힘들다는 단점이 있지만, 캡슐화가 아주 잘 지켜진 예시라고 할 수 있습니다.