Today's Codekata
class Solution {
public int solution(long num) {
int answer = 0;
if (num == 1) {
return 0;
} else {
while (num != 1) {
if (answer >= 500) {
return -1;
}
if (num % 2 != 0) {
num = num * 3 + 1;
} else {
num = num / 2;
}
answer++;
}
return answer;
}
}
}
if문이 너무 많은 느낌이다. 좀 더 간결하게 만들 수 있을 것 같은데, 아직 지식이 부족하다. 문제에서 반복 횟수가 500번을 넘어가면 -1을 반환해야 했는데, 주어진 수는 626331 이었고 이를 반복 계산하면 500번이 넘어감으로 -1을 반환해야했다. 근데 몇번을 돌려도 488번으로 결과값이 나와서 찾아보니 반복되는 과정에서 숫자가 int 범위를 초과한 걸로 보였다. 그래서 int를 long으로 변경해주는 것으로 정답에 도달할 수 있었다.
SELECT ANIMAL_TYPE,
CASE WHEN NAME IS NULL THEN 'No name'
ELSE NAME END AS NAME,
SEX_UPON_INTAKE
FROM ANIMAL_INS
ORDER BY ANIMAL_ID;
실행 결과는 이상 없었지만, 제출 시 계속 오답이 나와서 원인을 파악해보니 'No name'을 소문자로 쓴 것이 문제였다. 대소문자까지 정확하게 일치해야 하는 조건이었던 것이다. 오답 원인이 단순한 대소문자 차이여서 허탈했지만, 좋은 교훈이었다
키오스크 프로젝트
// 기존에 생성한 Menu의 MenuItem을 조회 할 때 스트림을 사용하여 출력하도록 수정
for (int i = 0; i <= 2; i++) {
System.out.println((i + 1) + ". " + menu.get(i).getCategory());
}
menu.stream()
.map(Menu::getCategory)
.forEach(category -> System.out.println(??? + ". " + category));
오늘 다시 도전해봐야할 과제는 반복문이 아닌 Stream 방식으로 출력하는 것이다.
Java Stream Index 이런 식으로 검색하다보니 AtomicInteger를 활용하라는 얘기가 많이 보여서 정리해봤다.
AtomicInteger
자바에서 안전하게 숫자를 증가시키거나 처리할 수 있는 객체형 정수 클래스이다. 여기서 객체형 정수란 단순 숫자 데이터가 아니라, 기능을 가진 객체로서 값을 조회하고 변경하는 다양한 메서드를 제공한다는 의미이다. 다양한 메서드들이 있었지만 자주 사용되는 것들만 정리해봤다.
주요 메서드
| 메서드 이름 | 설명 |
| get() | 현재 값을 반환 |
| set(int newValue) | 값 직접 설정 |
| getAndSet(int newValue) | 현재 값을 반환하고 새 값으로 설정 |
| incrementAndGet() | 값을 1 증가시킨 뒤 증가된 값 반환 |
| getAndIncrement() | 현재 값을 반환하고 그 뒤 1 증가 |
| addAndGet(int delta) | 지정한 수만큼 증가 후 증가된 값 반환 |
| getAndAdd(int delta) | 현재 값을 반환하고 지정한 수만큼 증가 |
| compareAndSet(expect, update) | 예상 값일 경우 새 값으로 변경 후 true 반환 |
그리고 AtomicInteger의 또 다른 특징은 멀티스레드 환경에서도 동기화 없이 사용할 수 있다는 것이다. 무슨 말이냐면 자바에서 여러 스레드가 동시에 하나의 변수를 수정해버리면, 그 값이 뒤섞이거나 손상되는 문제가 발생할 수 있는데, 여러 스레드가 동시에 값을 건드려도 AtomicInteger는 알아서 충돌 없이 잘 처리해준다는 뜻이다.
AtomicInteger menuIndex = new AtomicInteger(1);
menu.stream()
.map(Menu::getCategory)
.forEach(category -> System.out.println(menuIndex.getAndIncrement() + ". " + category));
이렇게 적용하니 1., 2., 3. ... 식으로 번호를 붙여가며 출력할 수 있었다. 다만 기존에는 최대 3개까지만 출력하고 있었는데, 위 방식엔 제한이 없어서 장바구니에 물건이 담길 때만 출력되는 메뉴들도 전부 출력되고 있어서 조건을 주기 위해 아래처럼 시도했다:
menu.stream()
.map(Menu::getCategory)
.filter(category -> menuNumber.get() <= 3)
.forEach(category -> System.out.println(menuNumber.getAndIncrement() + ". " + category));
처음에는 for문처럼 menu.size()을 기준 삼아야할 것 같아 아래처럼 시도했다.
.filter(category -> menu.size() <= 2)
그런데 제대로 동작하지 않아서 Stream 내부에서 컬렉션에 크기는 필터링 기준으로 삼을 수 없는 것인가 생각했었다. 근데 내가 설정한 조건을 살펴보니 조건에 도달하면 반복을 멈추게 만드는게 아니라 menu.size()가 2를 초과해버리면 아예 작동하지 않는 상태로 만들어버린 것이었다. for문에서처럼 '조건을 설정해두고 반복이 특정 조건에 도달하면 멈춘다' 라는 생각으로 작성했지만 실상은 '반복을 3번 하시오.' 또는 'menu.size() 즉 컬렉션의 크기가 2이하 일때만 동작하시오.' 이렇게 설정해버린 상황이었다. 단순 반복이 아닌 Stream을 사용하는 방식은 아직 익숙치 않지만, 조금씩 감이 생기는 것 같다.
AtomicInteger menuItemNumber = new AtomicInteger(1);
AtomicInteger index = new AtomicInteger();
menu.stream()
.map(Menu::getMenuItems)
.limit(items.size())
.forEach(menuItem ->
System.out.println(menuItemNumber.getAndIncrement() + ". " + formatMenu(items.get(index.getAndIncrement()))));
단순히 앞에서부터 정해진 수만큼 반복하는 작업이라면 .limit()을 사용하는 것이 더 직관적이고 안전하다는 걸 배웠다.
마치며
처음에는 Stream을 써보자 라는 단순한 목표였지만 점차 왜 그렇게 동작하는가에 대해 고민해보면서 코드의 구조에 대한 이해도가 커갔다. 오늘은 그런 시행착오를 겪으면서 Stream API의 동작 방식과 흐름 제어에 대해 더 깊이 이해할 수 있는 계기가 되었다. 내일은 IntStream을 활용해서 번호 출력이나 반복 흐름을 더 간결하게 만들어볼 예정이다. 오늘은 많은 것을 담아내지 못한 것 같은 아쉬움이 있다. 실무 개발자로 근무중이신 분의 이야기도 들어봤는데, 아직 갈 길이 멀다는게 느껴진다. 그때 그때 배우는 것들을 잘 기록해두고 필요할 때 볼 수 있도록 잘 정리해봐야겠다.