๐ ํ๋ก์ ํธ ๊ฐ์
1๏ธโฃํ๋ก์ ํธ๋ด์ฉ
์ฌํ, ์ฌ์ ์ ๊ธฐ๋กํ๋ SNS ์๋น์ค 3๋จ๊ณ
2๏ธโฃํ๋ก์ ํธ ์ฃผ์ ๋ฐ ํ์ ๊ตฌํ ๊ธฐ๋ฅ ์ ์
์ผ๋์
3๏ธโฃํ๋ก์ ํธ ๋ชฉ์
Spring Boot, DB ์ค๊ณ, DB ํธ๋์ญ์ , RESTful API ์ค๊ณ ๋ฅ๋ ฅ ํฅ์, Spring Security, JWT, JUnit, OpenApi ํ์ฉ
4๏ธโฃํ๋ก์ ํธ ๊ธฐ๊ฐ
2023๋ 11์ 10์ผ (์) ~ 11์ 16์ผ(๋ชฉ)
5๏ธโฃTeam Repository
https://github.com/FC-BE-ToyProject-Team8/TravelApp
GitHub - FC-BE-ToyProject-Team8/TravelApp: ์ฌํ ๊ธฐ๋ก ์๋น์ค SpringBoot REST API ์๋ฒ
์ฌํ ๊ธฐ๋ก ์๋น์ค SpringBoot REST API ์๋ฒ. Contribute to FC-BE-ToyProject-Team8/TravelApp development by creating an account on GitHub....
github.com
6๏ธโฃ๊ธฐ์ ์คํ
์ธ์ด
Java17
๊ฐ๋ฐํ๊ฒฝ ๋ฐ Dependency
Spring Boot 3.1.5
Gradle 8.3
MySQL 8
Spring Web
Spring Data JPA
JUint5
lombok
MapStruct
JWT
Redis
CI
GitHub Actions
API ๋ช ์ธ
Swagger
โ๏ธํ๋ก์ ํธ ์ปจ๋ฒค์
1๏ธโฃ์ฝ๋ฉ ์ปจ๋ฒค์
๋ธ๋ก์ ๋ค์ฌ์ฐ๊ธฐ๋ง 4๊ณต๋ฐฑ์ผ๋ก ์ปค์คํ ํ์ฌ ๊ตฌ๊ธ ์๋ฐ ์ฝ๋ฉ ์ปจ๋ฒค์ ์ ์ฌ์ฉ
Git Hub Actions CI๋ฅผ ํตํด ์ฒดํฌ ์คํ์ผ์ด ๋ง์ง ์๋๋ค๋ฉด Merge ๋ชปํ๋๋ก ๋ธ๋ฝํน
2๏ธโฃํ ์คํธ ์ฝ๋ ์ปจ๋ฒค์
Repository : @Query ์ด๋ ธํ ์ด์ ๋ฑ์ ์ฌ์ฉํ ๋ฉ์๋์ ๋ํด ํ ์คํธ ์ฝ๋ ์์ฑ ํ์
Service : ๋จ์ Repository ๋ฉ์๋ ํธ์ถ ์ด์์ ๋ก์ง์ ์ํํ๋ ๋ฉ์๋์ ๋ํด ํ ์คํธ ์ฝ๋ ์์ฑ ํ์
Controller : ๋ชจ๋ public ๋ฉ์๋ ํ ์คํธ ์ฝ๋ ์์ฑ ํ์
3๏ธโฃ์ํ์ฝ๋ ์ปจ๋ฒค์
- 200 : ๋ชจ๋ ์ฑ๊ณต
- 400 : ํด๋ผ์ด์ธํธ๊ฐ ์ ๋ ฅ์ ์๋ชปํ ๊ฒฝ์ฐ
- 401 : ์ธ์ฆ ์ ๋ณด๊ฐ ์๋ ๊ฒฝ์ฐ
- 403 : ์ ๊ทผ ๊ถํ์ด ์๋ ๊ฒฝ์ฐ
- 500 : ์๋ฒ ๋ด๋ถ ์๋ฌ
4๏ธโฃ์ปค๋ฐ ๋ฉ์ธ์ง ์ปจ๋ฒค์
์ปค๋ฐ ์ ๋ชฉ์ prefix: ์ปค๋ฐ ๋ฉ์์ง ํํ๋ก ์์ฑ
prefix ๋ ๊ฐ๊ฐ ์ํฉ์ ๋ง๋ prefix๋ฅผ ์ฌ์ฉ
(feat, fix, docs, test, refactor...)
๋ชจ๋ ์ปค๋ฐ ๋ฉ์ธ์ง๋ ํ๊ตญ์ด๋ก ์์ฑ
5๏ธโฃํ๋ก์ ํธ ๊น ๋ธ๋์น ์ ๋ต
GitFlow ์ ๋ต ( ํ๋ก์ ํธ ๊ท๋ชจ ํน์ง์ release, hotfix ๋ธ๋์น๋ ์๋ต)
feature/#
- ์ค์ ์์ ์ ํ๋ ๋ธ๋์น
- ์ด์ ๋ฒํธ๊ฐ 1์ด๋ผ๋ฉด feature/1๋ก ๋ง๋ค๋ฉด ๋๋ค.
- 'develop'์ ๋ฒ ์ด์ค ๋ธ๋์น๋ก ํ์ฌ ๋ง๋ค์ด์ผ ํ๋ค.
- ( ๋ธ๋์น ์์ฑ์ ๋ฒ ์ด์ค ๋ธ๋์น[ ์ฒดํฌ์์๋์ด์๋ ๋ธ๋์น ]๋ฅผ ๊ธฐ์ค์ผ๋ก ๋ง๋ค์ด์ง๋ค.)
- ์์ ์ด ์๋ฃ๋๋ฉด develop์ผ๋ก Pull Request๋ฅผ ๋ ๋ฆฐ๋ค.
- 4๋ช ์ Approve๋ฅผ ๋ฐ์๋ค๋ฉด Mergeํ๋ค.
develop
- ๋ค์ ๋ฒ์ ๊ฐ๋ฐ์ ์ํด main์ผ๋ก ๊ฐ๊ธฐ ์ ๊ธฐ๋ฅ ์ฝ๋๋ค์ ๋ชจ์๋๋ ๋ธ๋์น
- ์์ฑํ ๊ธฐ๋ฅ์ด ์ ์๋๋๋์ง ํ์ธํ๊ณ , main์ผ๋ก PR ๋ฐ Merge๋ฅผ ํ๋ฉด ๋๋ค.
- main์ผ๋ก Pull Request๋ฅผ ๋ ๋ฆด๋๋ Approve ๋ฐ๋ ๊ฒ์ ์ ํ์ฌํญ์ด๋ค.
main
- ์ค์ ์๋น์ค๋ฅผ ์ด์ํ ์ ์๋ ๋ธ๋์น
- main์ ๋ฐฐํฌ๊ฐ ๋๊ณ ๋์๋ ๊ธฐ๋ฅ์ด ์ ์๋ํ๋์ง ํ์ธํด์ผ ํ๋ค.
6๏ธโฃ๊ธฐํ ํ์ ์ฌํญ
ํ์ ๊ด๋ จ
- ๋ฐ์ผ๋ฆฌ ์คํฌ๋ผ: ๋งค์ผ ์ค์ 10:00 Slack์ ์งํ์ํฉ ๊ณต์
๐๊ธฐํ ๋ฐ ๋ด๋น ํํธ
ERD
API ๋ช ์ธ
๐โ๏ธ๋ด๋น ํํธ
์ข์์ ๋ฑ๋ก, ์ข์์ ์ทจ์ , ์ฌ์ฉ์ ๋๋ค์์ผ๋ก ์ฌํ ๊ฒ์
LikeService.java
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
|
import kr.co.fastcampus.travel.common.exception.DuplicatedLikeException;
import kr.co.fastcampus.travel.common.exception.InvalidLikeCancelException;
import kr.co.fastcampus.travel.domain.like.entity.Like;
import kr.co.fastcampus.travel.domain.like.repository.LikeRepository;
import kr.co.fastcampus.travel.domain.member.entity.Member;
import kr.co.fastcampus.travel.domain.member.service.MemberService;
import kr.co.fastcampus.travel.domain.trip.entity.Trip;
import kr.co.fastcampus.travel.domain.trip.service.TripService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Slf4j
@Transactional(readOnly = true)
public class LikeService {
private final TripService tripService;
private final LikeRepository likeRepository;
private final MemberService memberService;
@Transactional
public void saveLike(Long tripId, String memberEmail) {
Trip trip = findTrip(tripId);
Member member = findMember(memberEmail);
if (isExisted(trip, member)) {
throw new DuplicatedLikeException();
}
likeRepository.save(createLike(trip, member));
}
@Transactional
public void deleteLike(Long tripId, String memberEmail) {
Trip trip = findTrip(tripId);
Member member = findMember(memberEmail);
if (isExisted(trip, member)) {
likeRepository.deleteByTripAndMember(trip, member);
} else {
throw new InvalidLikeCancelException();
}
}
private Trip findTrip(Long tripId) {
return tripService.findById(tripId);
}
private Like createLike(Trip trip, Member member) {
return Like.builder()
.trip(trip)
.member(member)
.build();
}
private Member findMember(String memberEmail) {
return memberService.findMemberByEmail(memberEmail);
}
private boolean isExisted(Trip trip, Member member) {
return likeRepository.existsByTripAndMember(trip, member);
}
}
|
cs |
TripService.java
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
|
package kr.co.fastcampus.travel.domain.trip.service;
import java.util.List;
import java.util.stream.Collectors;
import kr.co.fastcampus.travel.common.exception.EntityNotFoundException;
import kr.co.fastcampus.travel.common.exception.MemberMismatchException;
import kr.co.fastcampus.travel.domain.member.entity.Member;
import kr.co.fastcampus.travel.domain.member.service.MemberService;
import kr.co.fastcampus.travel.domain.trip.entity.Trip;
import kr.co.fastcampus.travel.domain.trip.repository.TripRepository;
import kr.co.fastcampus.travel.domain.trip.service.dto.request.TripSaveDto;
import kr.co.fastcampus.travel.domain.trip.service.dto.request.TripUpdateDto;
import kr.co.fastcampus.travel.domain.trip.service.dto.response.TripDetailDto;
import kr.co.fastcampus.travel.domain.trip.service.dto.response.TripInfoDto;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class TripService {
private final TripRepository tripRepository;
private final MemberService memberService;
public List<TripInfoDto> findAllTrips() {
List<Trip> trips = tripRepository.findAll();
return trips.stream()
.map(TripInfoDto::from)
.collect(Collectors.toList());
}
@Transactional
public TripInfoDto addTrip(TripSaveDto dto, String memberEmail) {
Member member = findMember(memberEmail);
var trip = tripRepository.save(dto.toEntity(member));
return TripInfoDto.from(trip);
}
private Member findMember(String memberEmail) {
return memberService.findMemberByEmail(memberEmail);
}
public TripDetailDto findTripDetailById(Long id) {
var trip = tripRepository.findFetchDetailById(id)
.orElseThrow(EntityNotFoundException::new);
return TripDetailDto.from(trip);
}
@Transactional
public TripInfoDto editTrip(Long tripId, TripUpdateDto dto, String memberEmail) {
var trip = findById(tripId);
boolean isWriter = memberEmail.equals(trip.getMember().getEmail());
if (!isWriter) {
throw new MemberMismatchException();
}
Trip updateTrip = dto.toEntity();
trip.update(updateTrip);
return TripInfoDto.from(trip);
}
@Transactional
public void deleteTrip(Long id) {
var trip = findById(id);
tripRepository.delete(trip);
}
public Page<TripInfoDto> findTripsByNickname(String nickname, Pageable pageable) {
Member member = findMemberByNickname(nickname);
Page<Trip> trips = tripRepository.findTripByMember(member, pageable);
return trips.map(TripInfoDto::from);
}
private Member findMemberByNickname(String nickname) {
return memberService.findByNickname(nickname);
}
public Page<TripInfoDto> searchByTripName(String tripName, Pageable pageable) {
Page<Trip> trips = tripRepository.findAllByNameContainingIgnoreCase(tripName, pageable);
return trips.map(TripInfoDto::from);
}
public List<TripInfoDto> findTripsByLike(String email, Pageable pageable) {
var member = memberService.findMemberByEmail(email);
var trips = tripRepository.findByLike(member, pageable);
return trips.stream()
.map(TripInfoDto::from)
.collect(Collectors.toList());
}
public Trip findById(Long id) {
return tripRepository.findById(id)
.orElseThrow(EntityNotFoundException::new);
}
}
|
cs |
์คํ ๊ฒฐ๊ณผ
์ข์์ ๋ฑ๋ก
์ข์์ ์ทจ์
์ฌ์ฉ์ ๋๋ค์์ผ๋ก ์ฌํ ๊ฒ์
๐ค๊ฐ์ธ ํ๊ณ
๋จผ์ 3Layered architecture ์ ์ต์ํด์ ธ ๋น ๋ฅธ ์๊ฐ๋ด์ ๊ฐ๋ฐ์ ํ ๋ด ์์ ์ด ๋ฟ๋ฏํ๋ค. ์ด๋ค ์ผ์ด์ค์ ์ด๋ค ์์ธ์ฒ๋ฆฌ๋ฅผ ํด์ผํ ์ง๋ ๋ฐ๋ก๋ฐ๋ก ๋ ์ค๋ฅด๊ณ , ๋ก์ง์ ์ด๋ป๊ฒ ๊ตฌ์ํด์ผํ๋์ง๋ ์ค์ค๋ก์๊ฒ ํ์คํ ๋ฏฟ์์ด ์๊ฒผ๋ค. ์ด๋ฒ ํ ํ ์ด ํ๋ก์ ํธ๋ ๊ฐ๋ฐ์ ๋น ๋ฅธ ์์ผ๋ด์ ๋๋๋ค.
๋ค๋ง ์์ฌ์ด ์ ์ด ์กฐ๊ธ ์์๋ค. ์ข์์ ๋ ํผ๋ฐ์ค๋ฅผ ์ธ์คํ๊ทธ๋จ์ ํํธ๋ก ์๊ฐํ๋ค. ๋ฐ๋ผ์ ํธ๋ ๋๋ BTS ๋ฉค๋ฒ, ์ ๋ช ์ธํ๋ฃจ์ธ์ ๋ฑ๊ณผ ๊ฐ์ด ์ธ๊ธฐ ๋ง์ ์ฌ๋์ ํผ๋์ ์ข์์๋ ๋งค์ฐ ๋น ๋ฅด๊ฒ ์ฌ๋ผ๊ฐ๊ธฐ์ ์ข์์ ์์ ๋ํ ๋์์ฑ ์ ์ด๋ฅผ ํด๋ณด๋ ค ํ์ง๋ง ์คํจํ๋ค.
ํธ๋์ญ์ ๊ฒฉ๋ฆฌ์์ค ์ค์ ( @Transactional(isolation = Isolation.READ_COMMITTED) )๊ณผ ๋๊ด์ ์ธ ๋ฝ( @Lock(LockModeType.OPTIMISTIC) )์ ์ฌ์ฉํ์ง๋ง ์คํจํ๋ค. ํ ์คํธ ์ฝ๋์์ ์ค๋ ๋ ์๋ฅผ 10000์ผ๋ก ์ฃผ๊ณ ๋๋ ธ์ ๋ ์ฑ๊ณตํด์ ํด๊ฒฐํ ์ค ์์์ง๋ง, ๋ช ๋ฒ ๋ ์คํํ๋ฉด ์คํจ ์ผ์ด์ค๊ฐ ์๊ฒผ๋ค.
@Test
@DisplayName("์ข์์ ๋ฑ๋ก")
void saveLike() throws InterruptedException {
int threadCount = 10000;
ExecutorService executorService = Executors.newFixedThreadPool(32);
CountDownLatch latch = new CountDownLatch(threadCount);
//given
Long tripId = 1L;
Trip trip = createTrip();
given(tripService.findById(tripId)).willReturn(trip);
//when
for (int i = 0; i < threadCount; i++) {
executorService.submit(() -> {
try {
Member member = createMember();
likeService.saveLike(tripId, member.getEmail());
} finally {
latch.countDown();
}
});
}
latch.await();
//then
assertThat(trip.getLikeCount()).isEqualTo(threadCount);
}
์ํฐํฐ์ ๋ฒ์ (@Version)๋ ๋ค ๋ฃ๊ณ ํด๋ดค์ง๋ง ๋๋๋ด ํด๊ฒฐํ์ง ๋ชปํ์ฌ ๋๋ฌด ์์ฌ์ ๋ค.
๋ค์ ๋ฏธ๋ ํ๋ก์ ํธ๋ ์๋ฐ ์์ฝํํธ์ ๋ ๋์์ฑ ์ ์ดํ ๋งํ ์์๊ฐ ์๊ธฐ๋๋ฐ, ๋ด๊ฐ ๋์์ ํด๋ณด๊ฒ ๋ค๊ณ ํ ์์ ์ด๋ค. ์ด๋ ํ์คํ๊ฒ ๋์ผ๋ฉด ์ข๊ฒ ๋ค.
@PageableDefault(size = 5) Pageable pageable ๋ฅผ ์ฌ์ฉํ๋ฉฐ Pageable ์ ๋ํด์ ์ฒ์ ์๊ฒ ๋ผ์ ์ข์๋ค. ๊ธฐ์กด์ ํ์ด์ง๋ค์ด์ ๋ง ์๊ณ ์์๋๋ฐ, ์ด๋ฐ ๊ฐ๋ ฅํ ๊ธฐ๋ฅ์ด ์๋์ง ๋ชฐ๋๋ค. Pageable ๋๋ถ์ ์์์ DB์์ ํ์ด์ง๋ค์ด์ ๋ ์ฑ ๋์ค๋ ๊ฒ ์ ๋ง ์ข์๋ค.
๋ฒ์จ 1๋จ๊ณ, 2๋จ๊ณ, 3๋จ๊ณ ํด์ค๋ฉด์ ์ค์ค๋ก ๋ง์ด ์ฑ์ฅํ๋ค๋ ๊ฒ ๋๊ปด์ก๋ค. ํนํ ํ
์คํธ ์ฝ๋ ์์ฑ์์ ์ค๋ ฅ์ด ๋ง์ด ๋์์์ ๋๊ผ๋ค. ๋ํ ํด๋ฆฐ์ฝ๋ ๋ฐฉ๋ฒ๋ก ์ ์ต๋ํ ์ด๋ฆฌ๊ธฐ ์ํด ๋ฉ์๋๋ก ๋ฐ๋ก ๋นผ๊ณ , ๋ณ์ ๋ช
, ํด๋์ค ๋ช
, ๋ฉ์๋ ๋ช
ํ๋ํ๋ ์ ๊ฒฝ์ฐ๋ฉด์ ํ๋, ์ค๋ฌด์์๋ ์ ํ ์ ์์ ๊ฑฐ๋ ์๊ฐ์ด ๋ค์๋ค.(์ด์งธ์ 1๋จ๊ณ ์๋ฐ ์ฝ์ ํ๋ก์ ํธ๊ฐ ์ ์ผ ์ด๋ ค์ ๋ ๊ฑฐ ๊ฐ์ง ใ
ใ
ใ
ใ
ใ
)
์ข์ ํ์๋ค๊ณผ ํจ๊ปํ์ฌ ์ค๋ ฅ์ด ์ผ์ทจ์์ฅ ํ๋ค๊ณ ์๊ฐํ๋ค. ํจ๊ป ํ๋ 4๋ช ์ ํ์๋ค์๊ฒ ๊ฐ์ฌํจ์ ์ ํ๋ค.
๋๊ธ