Today's Codekata
package org.example;
public class Solution {
public boolean solution(int x) {
int number = x; // 자릿수 분리 과정에서 x의 값이 변경되므로 원본 값을 따로 저장
int sum = 0; // 자릿수 합계를 저장할 변수
while (x > 0) { // x의 각 자릿수를 반복해서 추출
sum += x % 10; // 현재 자릿수를 sum에 누적
x /= 10; // x를 10으로 나눠 다음 자릿수로 이동
}
return number % sum == 0; // 원래 숫자를 자릿수 합으로 나눠 나머지가 0이면 true 반환
}
}
// return 뒤의 수식의 결과값이 true면 true로 아니면 false로 반환이 가능하다.
// 단순히 true, false만 반환할 때는 유용할 것 같지만 if문처럼 추가적인 로직을 넣을 순 없어보인다.
SELECT SUM(CASE WHEN AGE IS NULL THEN 1 ELSE 0 END) AS USERS
FROM USER_INFO;
SELECT COUNT(*) AS USERS
FROM USER_INFO
WHERE AGE IS NULL;
IS NOT NULL 이 있으니 IS NULL 도 있겠다 생각을 해서 WHERE절에 AGE IS NULL 같은 방식으로 사용하려 했었는데, COUNT(컬럼명)은 기본적으로 NULL을 제외하고 카운팅한다는 내용을 보고, 위와 같이 NULL인 데이터에 1을 줘서 합산하는 방식을 통해 답을 찾았다. 그런데 더 공부해보니 COUNT()에 컬럼명이 아닌 *를 넣어주면 전체 행 수를 세기 때문에 NULL을 포함한다고하여 아래 코드처럼 사용이 가능함을 알게 되었다.
키오스크 프로젝트
public enum Discount {
veteran("국가유공자","10%", 0.1),
soldier("군인","5%", 0.05),
student("학생","3%", 0.03),
regular("일반","0%", 0);
private final String type;
private final String displayRate;
private final BigDecimal rate;
Discount(String type, String displayRate, double rate) {
this.type = type;
this.displayRate = displayRate;
this.rate = BigDecimal.valueOf(rate); // double로 받고 BigDecimal로 필드에 저장
}
public BigDecimal getRate() {
return rate;
}
public static void printDiscount() {
System.out.println("할인 정보를 입력해주세요.");
System.out.println("1. " + veteran.type + " : " + veteran.displayRate);
System.out.println("2. " + soldier.type + " : " + soldier.displayRate);
System.out.println("3. " + student.type + " : " + student.displayRate);
System.out.println("4. " + regular.type + " : " + regular.displayRate);
}
public static BigDecimal applyDiscount(BigDecimal totalPrice, int selectedDiscount) {
if (selectedDiscount == 1) {
return totalPrice.subtract(totalPrice.multiply(Discount.veteran.getRate()));
}
if (selectedDiscount == 2) {
return totalPrice.subtract(totalPrice.multiply(Discount.soldier.getRate()));
}
if (selectedDiscount == 3) {
return totalPrice.subtract(totalPrice.multiply(Discount.student.getRate()));
}
return totalPrice;
}
}
결제 시 다양한 할인 항목을 관리하고, 최종 금액에 할인율을 적용하는 기능을 Enum으로 구현했다. 처음에는 10%를 숫자 10으로 표현하고 출력 시 %를 붙이며 계산에선 0.1을 곱하는 방식으로 처리했지만, 출력용 값과 계산용 값을 나누는 게 더 효율적이라 판단해 displayRate와 rate를 각각 따로 만들었다. Enum은 따로 객체를 생성하지 않아도 바로 사용할 수 있어, Kiosk 클래스에서 매우 편리하게 활용할 수 있었다. 처음에는 감이 잘 안 잡혔는데, 국가유공자·군인·학생·일반 외에도 "여름 빅 세일!", "신규 회원 할인" 등 다양한 이벤트를 Enum 상수로 추가할 수 있다는 점에서, 할인 관련 로직을 전부 Discount 클래스 안에서 관리할 수 있는 구조가 됐다. 덕분에 Enum을 사용하는 목적과 장점을 제대로 이해할 수 있는 시간이었다. 또한 출력과 계산 로직을 각각 printDiscount()와 applyDiscount() 메서드로 분리했는데, 특정 Enum 값에서 호출할 필요 없이 Discount.printDiscount()처럼 클래스 단위에서 직접 사용할 수 있도록 static으로 선언해주었다. static은 클래스 자체에 속한 멤버를 의미하며, 객체 생성 없이 직접 호출할 수 있는 특징을 가지고 있다. 다만 현재 두 메서드 모두 할인 항목이 추가될 때마다 직접 코드를 수정해야 하므로, 열거된 상수들을 반복 처리할 수 있도록 리팩토링이 필요해 보인다.
for (MenuItem item : cartItems) { // 기존의 foreach문
System.out.println(formatMenu(item));
}
cartItems.stream().forEach(item -> System.out.println(formatMenu(item)));
// List<MenuItem> 타입인 cartItems → stream()을 통해 스트릠으로 변환한다.
// 이 스트림을 .forEach()로 반복하면서 각 요소 하나하나가 item 이라는 이름으로 들어온다.
// 리스트의 요소 타입에 따라 스트림의 람다 변수(item)의 타입도 따라온다.
// item은 cartItems라는 리스트에 들어있는 각각의 MenuItem을 의미하며, 스트림에서 하나씩 꺼내서 forEach() 내부에서 사용된다.
// 매개변수 -> 해당 매개변수를 바탕으로 수행할 작업
마지막 단계에서 Stream API를 활용하기 위해 공부해보고 있는데, 가장 쉽게 바꿔볼만한 부분을 찾아서 변경해보았다.
BigDecimal totalPrice = BigDecimal.ZERO;
for (MenuItem item : cartItems) {
totalPrice = totalPrice.add(item.getPrice());
}
cartItems.stream().forEach(item -> (totalPrice = totalPrice.add(item.getPrice())));
그러나 이 녀석은 똑같이 만들어도 오류가 발생했다. 람다식 안에서 사용되는 변수는 final이거나 사실상 final이어야 한다는 오류메시지가 totalPrice에서 확인된다. 말 그대로 스트림 내부의 람다 영역에서 외부 변수를 읽을 수는 있지만 수정할 수는 없다. 그 이유는 람다식은 내부적으로 지연 실행되거나 병렬 처리될 수 있으므로, 동기화 문제를 막기 위해 '불변 변수'만을 사용하도록 제한한다. 라고 되어있는데, 이 부분은 아직 이해가 어려운 영역인 듯하다. 그래서 아무래도 스트림을 다른 방식으로 활용해서 누적합된 총 가격을 반환하도록 만들어야 할 것 같다.
BigDecimal totalPrice = cartItems.stream()
.map(item -> item.getPrice())
.reduce(BigDecimal.ZERO, (sum, price) -> sum.add(price));
기존에는 totalPrice를 초기화해놓고 따로 for문을 작성했는데, 스트림을 활용할 땐 reduce() 함수에서 초기값을 입력하기에 바로 연결해서 작성할 수 있었다. 이걸 내가 해석한 바로는 이렇게 설명할 수 있을 것 같다. 먼저 장바구니 리스트에서 각 요소를 흐르게 하는 스트림을 생성하고, 임시 변수 item을 선언해서 그 item에 getPrice만 담는다. 그리고 스트림의 모든 요소를 누적하거나 합산할 때 사용하는 reduce(초기값, 누적 함수) 함수를 사용해서 초기값은 ZERO, 누적 함수는 sum(합계)에 price(각 메뉴의 가격)을 반복하여 더 해주는 람다식을 넣어주는 것이다. 그리고 람다식이 너무 단순해서 메서드 참조 표현식으로 변경이 가능하다는 메시지가 나오는데, 아직 메서드 참조는 정확히 어떤 것인지 이해하지 못 했다. 이 느낌을 뭔가 비유로 표현해보면 요리를 만들때 '먼저 재료를 썰고 -> 기름을 두르고 -> 후라이팬에서 볶으세요.' 같이 직접 요리 순서를 쓰는 방식이면 메서드 참조는 그 말처럼 이미 검증된 레시피를 그대로 쓰는 방식이라고 할 수 있겠다. 그래서 메서드 참조 표현식을 써보면 이런 식으로 만들어진다.
BigDecimal totalPrice = cartItems.stream() // 장바구니 리스트를 스트림으로 변환
.map(MenuItem::getPrice) // 메뉴 하나하나에서 가격만 꺼내기
.reduce(BigDecimal.ZERO, BigDecimal::add); // 가격들을 모두 더해서 최종 금액 구하기
MenuItem::getPrice 는 MenuItem Class에 있는 가격을 꺼내는 getPrice() 메서드를 그대로 쓰겠다는 의미로 해석되겠다. BigDecimal::add 또한 마찬가지로 BigDecimal끼리 더 할때 쓰는 add()라는 기능을 불러서 쓰겠다는 의미가 된다!
마치며
이번 프로젝트를 진행하면서 코드가 동작하는 것보다 코드를 설명할 수 있는 것이 얼마나 중요한지 실감했다. 코드를 내가 직접 설명해보면서 내가 하는 말과 내가 작성한 코드가 같은 말을 한다는 느낌을 받았다. 지금 내가 구현한 로직이 어떤 의미를 가지는지, 조금이나마 설명할 수 있는 단계로 가고 있는 것 같아 뿌듯하다. 한 줄 짜리 스트림을 이해하는 데 긴 시간이 소요됐지만, 그 시간들이 결코 낭비라 생각하지 않고 막연했던 개념들을 하나하나 내 것으로 만들어가는 과정이라 생각한다.