Today's Codekata
# Game Play Analysis IV
SELECT ROUND(
COUNT(DISTINCT a.player_id) /
(SELECT COUNT(DISTINCT player_id) FROM Activity), 2
) AS fraction
FROM Activity a
JOIN (
SELECT player_id, MIN(event_date) AS first_login
FROM Activity
GROUP BY player_id
) first
ON a.player_id = first.player_id
AND a.event_date = DATE_ADD(first.first_login, INTERVAL 1 DAY);
# Number of Unique Subjects Taught by Each Teacher
SELECT teacher_id, COUNT(DISTINCT subject_id) AS cnt
FROM Teacher
GROUP BY teacher_id;
# User Activity for the Past 30 Days I
SELECT activity_date AS day,
COUNT(DISTINCT user_id) AS active_users
FROM Activity
WHERE activity_date BETWEEN '2019-06-28' AND '2019-07-27'
GROUP BY activity_date;
문제를 너무 어렵게 생각하는 경향이 있는 것 같다. 지문을 잘 천천히 제대로 읽고 답을 찾아가는 것도 중요함을 느꼈다.
// 신고 결과 받기
class Solution {
public int[] solution(String[] id_list, String[] report, int k) {
Set<String> reports = new HashSet<>(Arrays.asList(report));
Map<String, Integer> reportedCount = new HashMap<>();
Map<String, List<String>> reporters = new HashMap<>();
for (String user : id_list) {
reportedCount.put(user, 0);
reporters.put(user, new ArrayList<>());
}
for (String s : reports) {
String[] split = s.split(" ");
reportedCount.put(split[1], reportedCount.get(split[1]) + 1);
reporters.get(split[1]).add(split[0]);
}
int[] answer = new int[id_list.length];
for (String s : id_list) {
if (reportedCount.get(s) >= k) {
for (String reporter : reporters.get(s)) {
int i = Arrays.asList(id_list).indexOf(reporter);
answer[i]++;
}
}
}
return answer;
}
}
`Map`이 어떤 식으로 동작하는지 더 이해할 수 있었고, `Arrays.asList()`는 배열을 리스트로 바꿔주는 메서드이다.
뉴스피드 프로젝트
Java 코드 컨벤션

어제 프로젝트를 진행하면서 팀원 각자가 자신만의 스타일로 코딩을 하다보니 전체적인 코드 기준이 없어서 가독성이 현저히 떨어지는 현상을 마주했다. 특히 파라미터 정렬 방식이나 메서드 내부에 줄넘김 처리 같은 세세한 부분에서 스타일이 제각각이었고, 이를 해석하는데 불필요한 시간이 소요됐다. 단순히 미관 상의 문제만이 아니라 협업의 효율성과 유지보수성도 떨어지게 만든다고 느꼈다. 이에 팀원들과 함께 각자의 스타일에 대해 이야기하며, 어떤 방식이 더 명확하고 일관성 있는지 의견을 나눴다. 코드 컨벤션이 단순한 형식이 아니라 팀의 언어와 같다는 생각이 들었다. 앞으로는 프로젝트 시작 시점에 코드 컨벤션을 함께 정립하는 시간이 꼭 필요하겠다고 깨달았다.
북마크 기능 구현
이번 뉴스피드 프로젝트에 북마크 기능을 추가하면서, 엔티티 간의 관계와 데이터 흐름을 최대한 제대로 이해해보려고 노력했다.
@Entity
@Getter
@NoArgsConstructor
public class Bookmark extends BaseEntity {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id", nullable = false)
private Post post;
public Bookmark(User user, Post post) {
this.user = user;
this.post = post;
}
}
`User` ↔ `Bookmark` ↔ `Post` 북마크는 특정 사용자가 특정 게시글을 저장한 상태를 나타내는 연결 테이블 같은 역할을 한다. 최근에 추가한 북마크순으로 볼 수 있게 만들기 위해 `BaseEntity`를 상속받았다.
public class BookmarkService {
// 필드
public void addBookmark(User user, Long postId) {
...
}
public void removeBookmark(User user, Long postId) {
...
}
public PostListResponse getBookmarks(User user, Pageable pageable) {
Page<Bookmark> bookmarks = bookmarkRepository.findByUser(user, pageable);
Page<Post> postPage = bookmarks.map(Bookmark::getPost);
List<PostResponse> postResponses = postMapper.toListResponse(postPage);
return new PostListResponse(
postResponses,
postPage.getNumber(),
postPage.getTotalPages(),
postPage.getTotalElements()
);
}
}
서비스 흐름을 정리해보면 아래와 같다.
1. 북마크 추가
`postId`로 게시글을 조회하고, 이미 해당 게시글에 대한 북마크가 존재하는지 확인한다. 없으면 새로운 `Bookmark`을 생성한다.
2. 북마크 삭제
`postId`로 게시글을 조히하고, 해당 사용자가 저장한 북마크를 검색해서 존재하면 삭제한다.
3. 북마크로 목록 조회
`findByUser(user, pageable)`로 북마크 목록을 조회하고, 가져온 `Page<Bookmark>`를 `.map(Bookmark::getPost)`를 통해 매핑해준다. 그리고 `PostMapper`를 활용해서 DTO로 변환 후 페이지 정보와 함께 반환해준다. (이 부분은 가장 어려웠다.)
마치며
오늘도 GitHub를 활용해 효율적이고 능동적인 협업을 이어갈 수 있었고, 다른 사람의 코드를 읽으며 이해하는 능력도 한층 성장한 것 같다. 저녁에는 튜터님의 피드백을 통해 그동안 놓쳤던 문제들을 마주할 수 있었고, 동시성 이슈, N+1 문제, 불필요한 코드와 같은 부분들을 새롭게 알게 되었다. 하루 동안 여러 상황을 고민하며 느낀 점은, ‘정답’이 꼭 하나만 존재하는 건 아니라는 것이다. 효율성만 본다면 더 나은 해법이 있을 수 있지만, 상황과 목적에 따라 코드는 여러 형태를 가질 수 있다는 점을 실감했다. 실무에서는 아마 더 그렇지 않을까 라는 생각을 해봤다. 내일은 내 코드만 보는 데서 그치지 않고, 팀이 함께 만들어가는 프로젝트인 만큼 모든 부분을 제대로 이해하고 짚어가는 시간을 가져야겠다.