Today's Codekata
// 가장 가까운 같은 글자
class Solution {
public int[] solution(String s) {
int[] answer = new int[s.length()];
for (int i = 0; i < s.length(); i++) {
answer[i] = -1;
for (int j = i - 1; j >= 0; j--) {
if (s.charAt(i) == s.charAt(j)) {
answer[i] = i - j;
break;
}
}
}
return answer;
}
}
처음엔 `boolean`을 활용해서 `if`문 조건에 부합할 땐 true로 변경해주고 false일 땐 -1을 넣는 식으로 하다가, 애초에 -1을 값으로 줘버리고 조건에 부합하면 덮어씌워버리는 방식으로 변경했다. 코드카타를 할 때마다 느끼지만 답을 찾는 방법은 정말 다양한 것 같다.
// 푸드 파이트 대회
class Solution {
public String solution(int[] food) {
String answer = "";
for (int i = 1; i < food.length; i++) {
for (int j = 0; j < food[i]/2; j++) {
answer += i;
}
}
answer += 0;
for (int i = food.length - 1; i > 0; i--) {
for (int j = 0; j < food[i]/2; j++) {
answer += i;
}
}
return answer;
}
}
처음에 `for`문을 일일이 6개나 만들어서 정답에 도달한 다음 반복문 안에 반복문을 넣는 방식으로 줄여서 완성했다. 지금 아는 지식 안에서는 최선이었던 것 같은데, 코드카타를 진행하면서 모르는 것들을 공부해서 답에 도달하는 게 좋은 것인지 가진 것을 활용해서 답에 도달하는 게 좋은 것인지 잘 모르겠다. 뭐든 나에게 다 도움이 되겠지만 그런 생각이 들었다.
-- 대여 기록이 존재하는 자동차 리스트 구하기
SELECT DISTINCT(C.CAR_ID) AS CAR_ID
FROM CAR_RENTAL_COMPANY_CAR C
INNER JOIN CAR_RENTAL_COMPANY_RENTAL_HISTORY H ON C.CAR_ID = H.CAR_ID
WHERE C.CAR_TYPE = '세단'
AND MONTH(H.START_DATE) = 10
ORDER BY 1 DESC
-- 모든 레코드 조회하기
SELECT *
FROM ANIMAL_INS
ORDER BY ANIMAL_ID
-- 즐겨찾기가 가장 많은 식당 정보 출력하기
SELECT FOOD_TYPE, REST_ID, REST_NAME, FAVORITES
FROM (
SELECT FOOD_TYPE, REST_ID, REST_NAME, FAVORITES,
RANK() OVER (PARTITION BY FOOD_TYPE ORDER BY FAVORITES DESC) AS RK
FROM REST_INFO
) A
WHERE RK = 1
ORDER BY FOOD_TYPE DESC;
마지막 문제와 같은 유형에서 조건 필터링이나 정렬을 하려면, 윈도우 함수로 생성한 값을 대상으로 `WHERE`, `ORDER BY` 등을 걸어줘야 하는데, 윈도우 함수는 바로 `WHERE`절에서 사용할 수 없다. 그래서 서브쿼리로 감싸서 그 안에서 만든 랭킹을 외부 쿼리에서 필터링하는 형태로 많이 사용된다.
Today I Learned
Database
데이터베이스 기초 개념
Database: 여러 사람이 공유하고 사용할 목적의 데이터 집합. DBMS에 의해 제어된다.
DBMS(Database Management System): 데이터 정의, 저장, 검색, 수정, 삭제 등을 효율적으로 수행. SQL로 관리된다.
DBMS 주요 기능: 데이터 정의, 관리, 보안, 트랜잭션 관리 (ACID), 백업/복구, 동시성 제어
트랜잭션 (Transaction)
데이터베이스의 논리적 작업 단위이다.
ACID 속성
- Atomicity: 전부 성공하거나 전부 실패
- Consistency: 일관성 유지
- Isolation: 서로 간섭 없음
- Durability: 영구 반영됨
RDBMS (관계형 데이터베이스)
데이터를 테이블 형태로 저장
테이블 = 행(row, record) + 열(column, field)
무결성 제약
- 엔터티 무결성 (PK)
- 참조 무결성 (FK)
- 도메인 무결성 (데이터 타입)
관계 설정: 1:1 / 1:N / N:M 관계 → 외래 키(Foreign Key)로 설정
키(Key): Primary Key, Foreign Key, Unique Key
SQL 기본 분류
| 분류 | 역할 |
| DDL | 구조 정의 (CREATE, ALTER, DROP) |
| DML | 데이터 조작 (INSERT, UPDATE, DELETE) |
| DQL | 조회 (SELECT) |
| DCL | 권한 제어 (GRANT, REVOKE) |
| TCL | 트랜잭션 제어 (COMMIT, ROLLBACK) |
MySQL 주요 자료형
숫자형
- TINYINT, INT, BIGINT 등 (SIGNED / UNSIGNED)
- DECIMAL, FLOAT, DOUBLE
날짜형
- DATE, TIME, DATETIME, TIMESTAMP
문자형
- CHAR, VARCHAR, TEXT, BLOB
- ENUM, SET
제약조건
| 제약조건 | 설명 |
| NOT NULL | NULL 허용 안 됨 |
| UNIQUE | 고유한 값 |
| PRIMARY KEY | 고유 식별자 (NOT NULL + UNIQUE) |
| FOREIGN KEY | 외래 키 설정 + CASCADE 옵션 |
| AUTO_INCREMENT | 자동 증가 |
| DEFAULT | 기본 값 설정 |
JDBC 기초 구조
DB 연결: DriverManager + Connection
SQL 실행: Statement, PreparedStatement
결과 처리: ResultSet
리소스 수동 반환 필요
웹 보안 개념
SQL Injection
- 입력값으로 SQL 조작 시도
- PreparedStatement, 입력 검증, Escape 처리로 방지
XSS (Cross Site Scripting)
- 악성 스크립트 주입
- 게시판 등 HTML 필드에
기초 과정 마무리 정리
Spring MVC 구조
DispatcherServlet (프론트 컨트롤러)
HandlerAdapter (어댑터 패턴)
ViewResolver (뷰 응답 처리)
Client → Server 데이터 전송 방법
GET + Query Parameter
POST + HTML Form (x-www-form-urlencoded)
HTTP Request Body
Server → Client 응답 방법
정적 리소스
View Template
HTTP Response Body
Spring 주요 어노테이션
@Controller, @RestController
@RequestMapping
@PathVariable, @RequestParam, @ModelAttribute, @RequestBody
@ResponseBody, ResponseEntity
HttpMessageConverter 역할
Layered Architecture 구성
Controller: 요청 및 응답 처리
Service: 비즈니스 로직
Repository: 데이터베이스 연동
DTO: 계층 간 데이터 전달
PreparedStatement 특징
쿼리 미리 컴파일
Statement에 비해 성능 우수
SQL Injection 대응 가능
Persistence Framework 특징
JDBC 기반
PreparedStatement 사용
자원 관리 자동화
SQL Mapper: JDBC Template
일정 관리 앱 마무리
선택 일정 조회 시 달린 댓글들 함께 조회
// 선택 일정 조회
@Transactional(readOnly = true)
public EventResponseDto findEventById(Long eventId) {
Event event = eventRepository.findById(eventId)
.orElseThrow(() -> new EntityNotFoundException("일정이 존재하지 않습니다."));
return new EventResponseDto(event);
}
이제 남은 과제는 두 가지이다. '댓글 생성 수 제한'과 '선택 일정 조회 시 해당 일정에 달린 댓글들 함께 조회'할 방법을 찾아야한다. 먼저 댓글들을 함께 조회하게 만들기 위해 Comment 엔티티에 `@ManyToOne`으로 연관관계를 만들어준 것처럼 반대로 Event 엔티티에 `@OneToMany` 어노테이션을 달아주면 되지 않을까 하는 생각으로 시작했다.
@OneToMany(mappedBy = "event", fetch = FetchType.LAZY)
private Comment comment;
@OneToMany(mappedBy = "event", fetch = FetchType.LAZY)
private List<Comment> comments;
위 처럼 Comment로 만들어줬더니 container type 이여야 한다는 오류가 보여져서 Comment들이 담긴 List, 즉 1:N에 맞게 처리했다. `(mappedBy = "event")`는 연관관계의 주인이 아님을 나타내는데, 실제 외래키를 관리하는 쪽인 Comment의 Event 필드를 기준으로 매핑하게 된다. JPA에서 연관관계의 주인이란 외래키를 관리하는 쪽을 뜻한다. 댓글이 일정에 속했다고 일정이 주인이 되는 것이 아니다.
// 선택 일정 조회
@Transactional(readOnly = true)
public EventResponseDto findEventById(Long eventId) {
List<Comment> comments = commentRepository.findByEvent_EventId(eventId);
Event event = eventRepository.findByCommentsAndEventId(comments, eventId)
.orElseThrow(() -> new EntityNotFoundException("일정이 존재하지 않습니다."));
return new EventResponseDto(event);
}
`findByCommentsAndEventId`라는 쿼리 메서드를 생성해서 불러오면 될까 했는데, 일단 반환타입도 올바르지 않고 알맞은 반환타입인 리스트로 수정한다고 한들 일정이 아닌 그 일정에 달린 댓글들만 조회하게 될 걸로 보여졌다.
@Query("select e from Event e join fetch e.comments where e.eventId = :eventId")
Optional<Event> findByWithComments(@Param("eventId") Long eventId);
어떻게 구현할까 고민하던 중 `@Query` 어노테이션을 쓰면 직접 쿼리문을 만들 수 있다고 해서 사용했는데, 풀어보면 아래와 같다.
`select e` SQL에서 사용하는 *과 달리 JPQL에선 객체를 반환함으로 객체 Event를 조회한다.
`join fetch e.comments` 해당 일정의 댓글을 즉시 로딩으로 함께 조회한다.
`where e.eventId = :eventId` :eventId는 파라미터 바인딩 값으로, 아래 `@Param("eventId")`를 통해 넘긴 값으로 치환된다.
`@Param("eventId")` 메서드 파라미터에 들어오는 값을 `:eventId` 위치에 바인딩 시켜주는 어노테이션이다.
여기서 정리하고 갈 것들이 아주 많아보인다.
Fetch 전략
JPA에서 엔티티 간 관계를 맺을 때, 관련 데이터를 언제 로딩할지 설정하는 방식이다. (fetch: 가지고 오다)
| 전략 | 설명 | 장점 | 단점 |
| LAZY (지연 로딩) | 연관된 객체는 필요할 때까지 조회 X | 성능 최적화, 필요 시에만 쿼리 발생 | 연관 데이터 접근 시 추가 쿼리 발생 |
| EAGER (즉시 로딩) | 연관된 객체도 함께 조회 | 코딩은 간단, 한 번에 모두 로딩 | 성능 저하 가능, N+1 문제 위험 |
대부분의 경우 `FetchType.LAZY`가 권장되는데, `LAZY` 전략을 썼다면 `JOIN FETCH`, `EntityGraph` 같은 기법으로 정교한 로딩을 해야 하고, `EAGER`가 예외적으로 필요한 경우는 연관 데이터를 항상 함께 써야 하거나 단순한 CRUD에서 연관 객체가 필수로 보여야 하는 경우에 사용한다.
일단 댓글이 없을 때 일정만 조회가 안되어서 `join` → `left join`으로 변경해줌으로 해결했고, 일정은 조회가 되는데 댓글이 보이지 않아서 살펴보니 EventResponseDto에서 댓글 정보를 꺼내오지 않으면 당연히 댓글이 보여지지 않는 것이었다.
private List<CommentResponseDto> comments;
public EventResponseDto(Event event) {
this.eventId = event.getEventId();
this.title = event.getTitle();
this.description = event.getDescription();
this.name = event.getName();
this.createdAt = event.getCreatedAt();
this.modifiedAt = event.getModifiedAt();
this.comments = event.getComments()
.stream()
.map(CommentResponseDto::new)
.toList();
}
댓글 응답DTO 형식 즉 클라이언트가 JSON형태로 정보를 받을 수 있도록 만든 리스트를 반환하는 필드를 선언해주고, List<Comment> 타입의 댓글들을 스트림을 통해 List<CommentResponseDto> 타입으로 변경해서 반환하도록 변경해줬다.

마침내 선택한 일정 조회 시 해당 일정에 달린 댓글들이 함께 나타나고, 응답도 JSON으로 보기 좋게 만들어졌다.
일정 별 댓글 개수 제한
long countByEvent_EventId(Long eventId);
// 댓글 생성
@Transactional
public CommentResponseDto saveComment(CommentRequestDto requestDto, Long eventId) {
if (commentRepository.countByEvent_EventId(eventId) >= 10) {
throw new RuntimeException("해당 일정에 더 이상 댓글을 작성할 수 없습니다.");
} else {
Event event = eventRepository.findById(eventId)
.orElseThrow(() -> new EntityNotFoundException("일정이 존재하지 않습니다."));
Comment comment = new Comment(
event, requestDto.getDescription(), requestDto.getName(), requestDto.getPassword());
Comment savedComment = commentRepository.save(comment);
return new CommentResponseDto(savedComment);
}
}
DB에 존재하는 댓글 수를 카운트해서 댓글 수를 조건으로 걸면 되겠다 생각해서 Comment 레포지토리에서 count를 적어보니 관련된 메서드들을 쭉 확인할 수 있었다. 그래서 바로 서비스 로직에 반영해본 결과 아주 쉽게 구현이 됐다! 쿼리 메서드나 쿼리 어노테이션을 통해 DB와 소통하는 방법을 알기 전에는 막막하기만 했는데, 생각보다 어렵지 않게 만들어낼 수 있어서 좋았다.
마치며
아직 나에게 실무적인 경험이나 지식이 없기 때문에 지금 코딩하고 있는 방식들이 좋은 방향인지 성능에는 어떤 영향을 주는지 궁금함이 생긴다. 이 궁금증이 더 나은 방향을 모색하게 만들고 공부하게 되는 동력이 되는 것 같다. 틀릴 수도 있지만 이렇게 구현하는 게 맞을까 라는 질문을 던지면서 자꾸 도전해보는 것이 나를 성장시킨다고 느낀다. 내일 프로젝트 제출 전에는 데이터 무결성에 대한 부분을 공부해보고 리팩토링해서 잘 마무리하고 싶은 마음이다.