Today's Codekata
// K번째 수
class Solution {
public int[] solution(int[] array, int[][] commands) {
int[] answer = new int[commands.length];
for (int i = 0; i < commands.length; i++) {
int[] arr = new int[commands[i][1] - commands[i][0] + 1];
int idx = 0;
for (int j = (commands[i][0] - 1); j < commands[i][1]; j++) {
arr[idx] += array[j];
idx++;
}
Arrays.sort(arr);
answer[i] += arr[commands[i][2] - 1];
}
return answer;
}
}
몇 번째 몇 번째 라는 말은 인덱스 넘버에선 항상 -1 해서 생각해야한다는 걸 다시금 느끼게 해준 문제였다. 배열과 변수 idx를 활용하려는 생각은 좋았으나 그것들을 반복문을 돌면서 적절한 위치에서 초기화해줘야했으나 마지막에야 알게 되었다.
// 두 개 뽑아서 더하기
class Solution {
public int[] solution(int[] numbers) {
List<Integer> results = new ArrayList<>();
for (int i = 0; i < numbers.length - 1; i++) {
for (int j = i + 1; j < numbers.length; j++) {
int sum = numbers[i] + numbers[j];
if (!results.contains(sum)) {
results.add(sum);
}
}
}
int[] answer = new int[results.size()];
for (int i = 0; i < results.size(); i++) {
answer[i] = results.get(i);
}
Arrays.sort(answer);
return answer;
}
}
1,2,3...번째 숫자와 2,3,4...번째 숫자를 더해가는 방식은 좋았으나 중복값들이 생성됨으로 answer배열의 인덱스 범위를 자꾸 초과하게 되어서 `.contains()`을 사용해봤다. 해당 메서드는 리스트나 컬렉션 안에 특정 값이 있는지 확인할 때 사용되는데, 위 코드에선 두 수의 합이 리스트 안에 포함되어 있지 않으면 리스트에 저장하는 방식으로 중복값이 저장되는 것을 방지했다. 그 후에 리스트를 다시 배열로 변환해서 반환하는 과정이 필요해서 중복값을 자동으로 걸러주는 자료구조인 `Set`을 활용하는 게 더 좋아보인다.
-- 조건에 맞는 사용자와 총 거래금액 조회하기
SELECT U.USER_ID, U.NICKNAME, SUM(B.PRICE) AS TOTAL_SALES
FROM USED_GOODS_BOARD B
INNER JOIN USED_GOODS_USER U ON B.WRITER_ID = U.USER_ID
WHERE B.STATUS = 'DONE'
GROUP BY 1
HAVING SUM(B.PRICE) >= 700000
ORDER BY 3;
-- 가격대 별 상품 개수 구하기
SELECT FLOOR(PRICE / 10000) * 10000 AS PRICE_GROUP,
COUNT(*) AS PRODUCTS
FROM PRODUCT
GROUP BY 1
ORDER BY 1;
-- 3월에 태어난 여성 회원 목록 출력하기
SELECT MEMBER_ID, MEMBER_NAME, GENDER,
DATE_FORMAT(DATE_OF_BIRTH, "%Y-%m-%d") AS DATE_OF_BIRTH
FROM MEMBER_PROFILE
WHERE MONTH(DATE_OF_BIRTH) = 3
AND TLNO IS NOT NULL
AND GENDER = 'W'
ORDER BY MEMBER_ID;
`HAVING`절은 집계 함수(`SUM, AVG, COUNT`)의 결과에 조건을 걸 때 사용하는 구문이다.
SQL 실행 순서 즉 DB가 내부적으로 처리하는 순서는 다음과 같다.
`SELECT` → `FROM ` → `WHERE ` → `GROUP BY ` → `HAVING ` → `ORDER BY`
`FLOOR(숫자 표현식)` 함수는 실수를 내림하여 정수로 만드는 수학 함수이다. 가격이나 점수 같은 값을 구간별로 나눌 때 유용하다.
자꾸 제시된 문제를 정확히 읽지 않음으로 인한 실수를 하고 있어서 제대로 확인하고 답을 도출해야겠다고 생각했다.
Today I Learned
List, Set, Map
코드카타에서 `Set`을 사용하면 중복값을 쉽게 처리할 수 있다는 것을 확인했으나 햇갈리는 부분이 많아서 비교 정리해봤다.
| 항목 | List | Set | Map |
| 중복 허용 | 허용 | 허용하지 않음 | 키 중복 불가, 값은 허용됨 |
| 인덱스 접근 | 가능 (get(index)) | 불가능 | 불가능 (키를 통해 접근) |
| 순서 유지 | 유지 (ArrayList, LinkedList) | LinkedHashSet만 유지 | LinkedHashMap만 유지 |
| 탐색 방식 | contains()로 값 탐색 | contains()로 값 탐색 | get(key), containsKey() 사용 |
| 주요 구현 클래스 | ArrayList, LinkedList | HashSet, TreeSet, LinkedHashSet | HashMap, TreeMap, LinkedHashMap |
`List`: 순서가 중요하고 중복을 허용해야 할 때 (예: 사용자 입력 히스토리, 댓글 목록)
`Set`: 중복 제거가 중요한 경우 (예: 태그 목록, 유니크한 아이디 모음)
`Map`: 키와 값을 연결해야 하는 경우 (예: 학생 이름 ↔ 점수, 제품 코드 ↔ 재고 수량)
일정 관리 앱 만들기
Query Method
Spring Data JPA에서 복잡한 SQL 없이 메서드 이름만으로 원하는 조회 쿼리를 자동 생성할 수 있게 해주는 기능이다.
인터페이스에 메서드를 정의할 때 특정 규칙에 맞는 이름을 사용하면, 자동으로 해당 메서드에 대한 SQL 쿼리를 생성해준다.
| 대표 키워드 | 기능 |
| findBy | 조회 |
| countBy | 개수 반환 |
| existsBy | 존재 여부 확인 |
| deleteBy | 삭제 |
| ...Between, ...GreaterThan, ...LessThan | 범위 조건 |
| ...Containing, ...StartingWith, ...EndingWith | 문자열 검색 |
| ...And, ...Or | 복합 조건 |
List<Event> events = eventRepository.findAll(Sort.by(Sort.Direction.DESC, "modifiedAt"));
List<Event> events = (name == null)
? eventRepository.findAllByOrderByModifiedAtDesc()
: eventRepository.findByNameOrderByModifiedAtDesc(name);
`Query Method`를 활용해서 코딩해봤다. 삼항연산자도 사용해서 이름이 있을 때는 이름으로 조회하고 없을 땐 전체 목록을 조회하는 형태이다.
public interface CommentRepository extends JpaRepository<Comment, Long> {
List<Comment> findByEvent_EventId(Long eventId);
Optional<Comment> findByEvent_EventIdAndCommentId(Long eventId, Long commentId);
}
댓글 쪽에도 메서드를 만들어줬다. 위 코드의 2가지 `Query Method` 중에 아래 코드를 SQL문으로 풀어보면 아래처럼 될 것이다.
SELECT *
FROM COMMENT
WHERE event_id = "내가 입력한 id값" AND comment_id = "내가 입력한 id값";
예외 처리
@GetMapping
public ResponseEntity<?> getEvents(
@RequestParam(required = false) String name
) {
try {
return new ResponseEntity<>(eventService.findAllEvents(name), HttpStatus.OK);
} catch (EntityNotFoundException e) {
return ResponseEntity.status(HttpStatus.NO_CONTENT).body(e.getMessage());
}
}
// 전체 일정 조회
@Transactional(readOnly = true)
public List<EventResponseDto> findAllEvents(String name) {
List<Event> events = (name == null)
? eventRepository.findAllByOrderByModifiedAtDesc()
: eventRepository.findByNameOrderByModifiedAtDesc(name);
if (events.isEmpty()) {
throw new EntityNotFoundException("등록된 일정이 없습니다.");
}
return events.stream().map(EventResponseDto::new).toList();
}
댓글을 생성해주면서 event 테이블을 한번 최기화해주고, 빈 테이블을 조회해봤더니 `200 OK` 상태 코드가 출력되어서 당황했다. 살펴보니 컨트롤러 쪽에서는 예외를 catch하도록 만들어뒀으나 정작 어디서 예외가 발생한 건지 명시해주지 않아서 서비스가 사이즈 1짜리 리스트가 반환된 걸로 보인다. if문을 써서 예외를 던져주니 제대로 동작한다.
마치며
새로운 것들을 배울 때마다 다시 찾아볼 때를 생각해서 잘 정리하는 것이 중요하다는 걸 느낀다. 정리하는 과정에서 기억에도 더 남는 것 같다. 오늘은 특히 `Query Method`의 사용하는 방법이 아주 좋았다. SQL문을 조금씩 연습해오고 있었는데, 그것들이 드디어 연결되는 느낌을 받았다. 복잡한 쿼리문을 쓰는게 아니라 간편하게 메서드 형식으로 만들어서 사용할 수 있다는 것에 감탄했다. 지금의 개발자들은 많은 편리함이 갖춰진 가운데서 개발을 하고 있다는 지각을 하는 시간이었다. AI와 수많은 라이브러리, 프레임워크까지 다양한 도구들을 능숙하게 다루는 능력이 중요해보인다. 내일은 생성 댓글 수 제한과 댓글쪽이 아닌 일정을 조회했을 때 달린 댓글들을 함께 조회할 수 있도록 구현해야한다. 여느 때와 같이 시작 전에는 길이 보이지 않지만 가다보면 도착할 것이다.