일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- 인터프린터언어
- java map 저장
- 프로그래머스
- java알고리즘문제풀이
- 코딩부트캠프후기
- java 자료구조 활용
- 작은수제거하기
- java set 출력
- 노베이스부트캠프
- java set 저장
- 프로그래머스제일작은수
- java최솟값구하기
- 항해99후기
- java map 출력
- 격파르타장점
- 격파르타합격후기
- java기본자료형
- javaJRE
- 컴파일
- 항해15기
- javaJVM
- java list 출력
- java map
- 격파르타후기
- java list 저장
- sqld자격증합격
- java참조자료형
- 비전공자sqld
- java알고리즘
- 격파르타비전공자
- Today
- Total
코딩과 결혼합니다
230809 - 저장된 태그들로 인기 순위 태그 조회하기 본문
2차 ERD 수정
tag 검색 기능을 추가하기에 앞서 테이블을 분리해야 할 필요성을 느끼게 되어 Post 테이블에서 location_tag 테이블과 purpose_tag 테이블로 따로 빼냈다.
[테이블을 분리한 이유]
Post 태그에서 위치태그와 목적태그를 각각 분리하였다. 테이블을 분리하면 쿼리가 조금 복잡해지거나 여러 개의 테이블을 조인해야 하는 경우 성능에도 문제가 발생할 가능성이 있지만 이렇게 따로 만든 이유는
- 데이터 중복 최소화 및 데이터 일관성 유지
시간이 갈수록 중복되는 위치태그, 목적태그 데이터가 엄청나게 많아질 것이다. 성능 저하 우려. +Null도 허용되 때문에 이 부분도 고려해봄 ->Null 값을 그대로 넣을 수 없으므로, 입력 하지 않음 등으로 처리. - 복합 태그 처리
위치태그와 목적태그를 함께 저장하는 경우, 각 태그의 종류 및 의미를 구분하고 관리하는 복잡성이 있을 수 있다. 예를 들어 “산”이라는 단어가 위치태그로 사용될 수 도 있고 목적태그로 사용될 수도 있다. - 로직에 맞게 (& 중복 최소화)
게시물을 업로드할 때 게시물 작성자가 사용한 태그가 테이블에 있는지 확인하고 없으면 새로 삽입한다.
->태그 테이블을 따로 만들어야 함. - 캐싱 효과 극대화
테이블을 분리하면 캐싱 시 특정 태그 카테고리에 대한 캐시를 더욱 효율적으로 구성할 수 있다.
테이블 분리한 후 연관관계 & 받아온 태그를 해당 태그 테이블에 저장하는 로직
테이블 분리
//LocationTag entity
@Entity
@Getter
@NoArgsConstructor
public class LocationTag {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "l_tag_id")
private Long id;
@Column(nullable = true)
private String locationTag;
public LocationTag(String locationTagName) {
this.locationTag = locationTagName;
}
}
//PurposeTag entity
@Entity
@Getter
@NoArgsConstructor
public class PurposeTag {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "p_tag_id")
private Long id;
@Column(nullable = true)
private String purposeTag;
public PurposeTag(String purposeTagName) {
this.purposeTag = purposeTagName;
}
}
테이블 연관관계
Post는 하나씩의 태그들만 가지나 태그는 여러 개의 Post에 쓰일 수 있다.
Post와 각각의 태그 테이블은 (N : 1)의 관계이다.
//Post entity 中
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "l_tag_id")
private LocationTag locationTag;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "p_tag_id")
private PurposeTag purposeTag;
일단 단방향으로 연관관계는 Post 쪽에만 걸어두었다.
받아온 태그를 해당 태그테이블에 저장하는 로직
//PostController
//게시글 생성
@PostMapping
@ResponseStatus(HttpStatus.OK)
public MessageResponseDto uploadFile(
@RequestPart("post") PostRequestDto requestDto,
@RequestPart("photos") List<MultipartFile> photos
) throws IOException {
return postService.upload(requestDto, photos);
}
클라이언트가 "post"와 "photos"라는 파트로 요청을 보내도록 하였고, request에는 게시글 관련 정보가, photos 에는 사진 파일들이 전송되어야 한다. 다음 파일 처리 중 발생할 수 있는 예외를 처리하기 위해 IOException을 걸어주었다.
게시글을 생성하고 관련된 로직을 수행한 후, 결과로 MessageResponseDto 객체를 반환한다.
//PostService
//upload
public MessageResponseDto upload(PostRequestDto requestDto, List<MultipartFile> photos) throws IOException {
//사진 정보 처리, S3에 사진 업로드, 업로드한 사진 정보를 리스트에 추가
// ...
}
//locationTag 처리
LocationTag locationTag = processLocationTag(requestDto.getLocationTag());
// purposeTag 처리
PurposeTag purposeTag = processPurposeTag(requestDto.getPurposeTag());
//post 저장
Post post = new Post(requestDto);
post.setLocationTag(locationTag);
post.setPurposeTag(purposeTag);
postRepository.save(post);
// 업로드한 사진 정보와 게시글을 연결하여 저장
// ...
}
return new MessageResponseDto("파일 저장 성공");
}
//PostService
//위치, 목적 태그를 처리하는 메소드
private LocationTag processLocationTag(String locationTagName) {
if (locationTagName == null || locationTagName.isEmpty()) {
return null; // locationTag가 없는 경우
}
Optional<LocationTag> existingLocationTag = locationTagRepository.findByLocationTag(locationTagName);
return existingLocationTag.orElseGet(() -> locationTagRepository.save(new LocationTag(locationTagName)));
}
private PurposeTag processPurposeTag(String purposeTagName) {
if (purposeTagName == null || purposeTagName.isEmpty()) {
return null; // purposeTag가 없는 경우
}
Optional<PurposeTag> existingPurposeTag = purposeTagRepository.findByPurposeTag(purposeTagName);
return existingPurposeTag.orElseGet(() -> purposeTagRepository.save(new PurposeTag(purposeTagName)));
}
if (locationTagName == null || locationTagName.isEmpty())
위치 태그의 이름이 비어있거나(null이거나 비어있는 문자열이면) 위치 태그가 없는 것으로 간주하고 null을 반환한다.
Optional <LocationTag> existingLocationTag = locationTagRepository.findByLocationTag(locationTagName);
데이터베이스에서 해당 이름의 위치 태그를 조회한다.
return existingLocationTag.orElseGet(() -> locationTagRepository.save(new LocationTag(locationTagName)));
조회된 위치 태그가 있으면 그것을 반환하고, 없는 경우에는 새로운 위치 태그를 생성하여 데이터베이스에 저장한 후 반환한다.
processPurposeTag의 코드설명은 생략
왜 태그를 처리하는 메서드를 따로 만들어 주었나?
유지보수 용이성: 태그 처리 로직이 변경되어야 할 때, 이를 메서드 내에서만 수정하면 됩니다. 이렇게 하면 해당 로직을 사용하는 모든 곳에서 일일이 수정하지 않아도 되므로 유지보수가 간편해집니다.
단일 책임 원칙 적용: 메서드를 따로 만들어 특정한 기능을 처리하도록 분리하면, 클래스의 역할이 명확해집니다. 예를 들어, PostService 클래스는 게시글과 관련된 기능만을 담당하고, 태그 처리와 같은 세부적인 로직은 별도의 메서드로 분리하여 단일 책임 원칙을 준수할 수 있습니다.
테스트 용이성: 태그 처리 로직이 독립된 메서드로 분리되면, 이 메서드를 단위 테스트하기가 더 쉬워집니다. 로직이 간단해지고 의존성을 줄일 수 있기 때문입니다
가독성 향상: 태그 처리 로직이 메서드 내에서 독립적으로 정의되면, 해당 메서드의 이름만으로도 어떤 역할을 하는지 쉽게 이해할 수 있습니다.
즉 태크 처리와 같이 반복적으로 사용되는 로직은 메서드로 따로 분리하여 관리하면 코드의 품질과 유지보수성을 향상할 수 있다고 한다.
고마워요 GPT...
인기 순위 태그 조회하기
저장된 태그들 중에 가장 많이 쓰이는 태그들을 순서대로 정렬하여 List를 뽑아내면, 프론트에서는 이 api를 통해 수시로 요청을 보내어 인기 순위 태그 List를 받아가서 위와 같이 사용자에게 보여줄 것이다.
//TagController
//인기 순위 태그 가져오기
@GetMapping
public List<LocationTagResponseDto> locationTags(){
return tagService.rankNumber();
}
//TagService
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class TagService {
private final PostRepository postRepository;
public List<LocationTagResponseDto> rankNumber() {
Map<String, Integer> idFrequencyMap = new HashMap<>(); // 태그와 빈도수를 저장하는 맵
List<LocationTagResponseDto> locationTagResponseDtos = new ArrayList<>(); // 결과 DTO를 담을 리스트
List<Post> postList = postRepository.findAll(); // 모든 게시글 조회
for (Post post : postList) {
if(Objects.isNull(post.getLocationTag())){
continue;
}
String id = post.getLocationTag().getLocationTag();
idFrequencyMap.put(id, idFrequencyMap.getOrDefault(id, 0) + 1);
}
List<Map.Entry<String, Integer>> sortedEntries = new ArrayList<>(idFrequencyMap.entrySet());
sortedEntries.sort((entry1, entry2) -> entry2.getValue().compareTo(entry1.getValue()));
List<String> rankedIds = new ArrayList<>();
for (Map.Entry<String, Integer> entry : sortedEntries) {
rankedIds.add(entry.getKey());
}
for (int i = 0 ; i < rankedIds.size(); i++) {
if(i >= 6){
break;
}
locationTagResponseDtos.add(new LocationTagResponseDto(rankedIds.get(i)));
}
return locationTagResponseDtos;
}
}
가장 많이 사용된 태그를 순위별로 추출하는 메서드로 LocationTagResponseDto를 List 형태로 반환해 준다. LocationTagResponseDto 는 locationTag(위치태그) 필드(속성)를 가지고 있다.
//LocationTagResponseDto
@Getter
public class LocationTagResponseDto {
private String locationTag;
public LocationTagResponseDto(String locationTag){
this.locationTag = locationTag;
}
}
for (Post post : postList) {
if(Objects.isNull(post.getLocationTag())){
continue;
}
String id = post.getLocationTag().getLocationTag();
idFrequencyMap.put(id, idFrequencyMap.getOrDefault(id, 0) + 1);
}
post 를 하나씩 돌면서 locationTag 중에 null 인 게 있으면 continue 한다. null 이 아닌 tag들은 가져와서 id에 저장한다.
.put 으로 map의 key에는 LocationTag를 value 에는 빈도수를 담는다.
getOrDefault(id, 0)를 사용하여 해당 키가 맵에 이미 존재하는지 확인하고, 만약 존재하지 않으면 기본값으로 0을 반환하고 존재하면 해당 키와 연결된 값을 1 증가시킨다. 이렇게 함으로써 각 위치 태그가 몇 번 사용되었는지 빈도수를 계산하게 된다.
List<Map.Entry<String, Integer>> sortedEntries = new ArrayList<>(idFrequencyMap.entrySet());
sortedEntries.sort((entry1, entry2) -> entry2.getValue().compareTo(entry1.getValue()));
빈도수로 정렬된 위치 태그를 담는 리스트인 'sortedEntries'를 생성하는 코드이다. 여기서 사용되는 entrySet() 메서드는 맵의 키와 값을 쌍으로 하는 집합(Set)을 반환한다. 각 쌍은 'Map.Entry'라는 인터페이스로 표현된다.
맵은 키(key)와 값(value)으로 구성된 데이터 구조를 나타내는데, 키는 고유한 값이어야 하며, 키를 통해 값을 검색하거나 수정할 수 있다. Map.Entry 인터페이스는 이러한 키-값 쌍을 하나의 객체로 나타내기 위한 용도로 사용된다.
Map.Entry는 다음과 같은 주요 메서드들을 제공한다:
-
getKey(): 이 엔트리의 키를 반환
-
getValue(): 이 엔트리의 값(value)을 반환
-
setValue(V value): 이 엔트리의 값(value)을 주어진 값으로 설정
이렇게 반환된 'entrySet()'은 맵에 있는 모든 키와 값의 쌍을 담고 있으며, 이것을 정렬하려 사용하기 위해 'sortedEntries' 리스트에 'entrySet()'의 결과를 복사한다.
sortedEntries.sort((entry1, entry2) -> entry2.getValue().compareTo(entry1.getValue())); 라인은 sortedEntries 리스트를 빈도수를 기준으로 내림차순으로 정렬하는 코드. 람다식을 이용해서 entry1, entry2로 지정하고 entry1 이 크면 위치 교환.
List<String> rankedIds = new ArrayList<>();
for (Map.Entry<String, Integer> entry : sortedEntries) {
rankedIds.add(entry.getKey());
}
Map 자료구조의 Entry를 통해 key와 value 값을 쌍으로 가져온다. 다음 enty.getKey()로 해당 키 값을 가져와서 rankedIds List에 저장.
for (int i = 0 ; i < rankedIds.size(); i++) {
if(i >= 6){
break;
}
locationTagResponseDtos.add(new LocationTagResponseDto(rankedIds.get(i)));
}
return locationTagResponseDtos;
정렬하여 가져온 List를 n개로 제한해서 가져오기 위해 for 문을 돌렸고, 이를 통해 1~n의 순위인 locationTag를 locationTagResponseDto에 저장한다. 여기서는 6개로 제한하고 있다.
'코딩과 매일매일♥ > Seoulvival' 카테고리의 다른 글
230815 - 코드 리팩토링 멋진 3중 for문을 하나의 for문으로 (2) | 2023.08.15 |
---|---|
230810 - 태그를 누르면 해당 태그의 post가 조회되도록 하기 (0) | 2023.08.10 |
230806 - DB 정규화 하기 (0) | 2023.08.06 |
230804 - SpringBoot 에서 JPA 환경설정 / 기술 스택 선정 이유 (0) | 2023.08.04 |
230803 - 파이널 기획 및 S.A (기술 스택 선정과 이유), 오늘 한 일 (0) | 2023.08.04 |