FastCampus X Yanolja TechSchool

ํŒจ์ŠคํŠธ์บ ํผ์ŠคX์•ผ๋†€์ž: ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ ๋ถ€ํŠธ ์บ ํ”„ - ๐ŸŽ Spring ๊ธฐ๋ฐ˜ ํ† ์ด ํ”„๋กœ์ ํŠธ 2

ํ”„๋กœ๊ทธ๋ž˜๋จธ ์˜ค์›” 2023. 11. 2.

๐ŸŽ ํ”„๋กœ์ ํŠธ ๊ฐœ์š”

 

1๏ธโƒฃํ”„๋กœ์ ํŠธ๋‚ด์šฉ

์—ฌํ–‰, ์—ฌ์ •์„ ๊ธฐ๋กํ•˜๋Š” SNS ์„œ๋น„์Šค 2๋‹จ๊ณ„

2๏ธโƒฃํ”„๋กœ์ ํŠธ ์ฃผ์ œ ๋ฐ ํ•„์ˆ˜ ๊ตฌํ˜„ ๊ธฐ๋Šฅ ์ œ์•ˆ

์•ผ๋†€์ž

3๏ธโƒฃํ”„๋กœ์ ํŠธ ๋ชฉ์ 

Spring Boot, DB ์„ค๊ณ„, DB ํŠธ๋žœ์žญ์…˜, RESTful API  ์„ค๊ณ„ ๋Šฅ๋ ฅ ํ–ฅ์ƒ

4๏ธโƒฃํ”„๋กœ์ ํŠธ ๊ธฐ๊ฐ„

2023๋…„ 10์›” 23์ผ ~ 2023๋…„ 10์›” 29์ผ

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

CI

GitHub Actions

API ๋ช…์„ธ

Swagger

7๏ธโƒฃ์„œ๋ฒ„ ๋ฐฐํฌ

AWS EC2๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ develop , main ๋ธŒ๋žœ์น˜ ํ…Œ์ŠคํŠธ ์„œ๋ฒ„ ๋ฐ  ๋ฉ”์ธ ์„œ๋ฒ„๋กœ ์ž๋™ ๋ฐฐํฌ

 


 

 

โš–๏ธํ”„๋กœ์ ํŠธ ์ปจ๋ฒค์…˜

 

๋ชจ๋“  ์•ฝ์† ๋‚ด์šฉ์€ ๋…ธ์…˜ํŽ˜์ด์ง€์— ์ •๋ฆฌ

 

 

 

๋ชจ๋“  ํšŒ์˜ ๋‚ด์šฉ์€ ํšŒ์˜๋ก ์ž‘์„ฑ

 

1๏ธโƒฃ์ฝ”๋”ฉ ์ปจ๋ฒค์…˜

๋ธ”๋ก์˜ ๋“ค์—ฌ์“ฐ๊ธฐ๋งŒ 4๊ณต๋ฐฑ์œผ๋กœ ์ปค์Šคํ…€ํ•˜์—ฌ ๊ตฌ๊ธ€ ์ž๋ฐ” ์ฝ”๋”ฉ ์ปจ๋ฒค์…˜์„ ์‚ฌ์šฉ

Git Hub Actions CI๋ฅผ ํ†ตํ•ด ์ฒดํฌ ์Šคํƒ€์ผ์ด ๋งž์ง€ ์•Š๋Š”๋‹ค๋ฉด Merge ๋ชปํ•˜๋„๋ก ๋ธ”๋ฝํ‚น

 

2๏ธโƒฃํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ปจ๋ฒค์…˜

Repository : @Query ์–ด๋…ธํ…Œ์ด์…˜ ๋“ฑ์„ ์‚ฌ์šฉํ•œ ๋ฉ”์„œ๋“œ์— ๋Œ€ํ•ด ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ ํ•„์ˆ˜

Service : ๋‹จ์ˆœ Repository ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ ์ด์ƒ์˜ ๋กœ์ง์„ ์ˆ˜ํ–‰ํ•˜๋Š” ๋ฉ”์„œ๋“œ์— ๋Œ€ํ•ด ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ ํ•„์ˆ˜

Controller : ๋ชจ๋“  public ๋ฉ”์„œ๋“œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ ํ•„์ˆ˜

 

3๏ธโƒฃ์ƒํƒœ์ฝ”๋“œ ์ปจ๋ฒค์…˜

200 : ๋ชจ๋“  ์š”์ฒญ ์„ฑ๊ณต ์ƒํƒœ์ฝ”๋“œ

 

400 : ๋ฐ”์ธ๋”ฉ ์—๋Ÿฌ ๋ฐ ์ž˜๋ชป๋œ ์š”์ฒญ์„ ํฌํ•จํ•œ ๋ชจ๋“  ํด๋ผ์ด์–ธํŠธ ์—๋Ÿฌ

 

500 : ๋ชจ๋“  ์„œ๋ฒ„ ๋‚ด๋ถ€ ์—๋Ÿฌ

 

4๏ธโƒฃ์ปค๋ฐ‹ ๋ฉ”์„ธ์ง€ ์ปจ๋ฒค์…˜

์ปค๋ฐ‹ ์ œ๋ชฉ์€ prefix: ์ปค๋ฐ‹ ๋ฉ”์‹œ์ง€ ํ˜•ํƒœ๋กœ ์ž‘์„ฑ

 

prefix ๋Š” ๊ฐ๊ฐ ์ƒํ™ฉ์— ๋งž๋Š” prefix๋ฅผ ์‚ฌ์šฉ
(feat, fix, docs, test, refactor...)

๋ชจ๋“  ์ปค๋ฐ‹ ๋ฉ”์„ธ์ง€๋Š” ํ•œ๊ตญ์–ด๋กœ ์ž‘์„ฑ

 

5๏ธโƒฃํ”„๋กœ์ ํŠธ ๊นƒ ๋ธŒ๋žœ์น˜ ์ „๋žต

GitFlow ์ „๋žต ( ํ”„๋กœ์ ํŠธ ๊ทœ๋ชจ ํŠน์ง•์ƒ release, hotfix ๋ธŒ๋žœ์น˜๋Š” ์ƒ๋žต)

์ž‘์„ฑ์ž : me~

 

 


 

โš’๏ธํ”„๋กœ์ ํŠธ ์„ค๊ณ„

1๏ธโƒฃAPI ์„ค๊ณ„

๋ฉ”์ธ ์„œ๋ฒ„ Swagger page

 

 

2๏ธโƒฃํ”„๋กœ์ ํŠธ ERD

 

 

3๏ธโƒฃํ”„๋กœ์ ํŠธ Entity

@Embadded๋ฅผ ๋ณด์—ฌ์ฃผ๊ธฐ ์œ„ํ•œ ํด๋ž˜์Šค ๋‹ค์ด์–ด๊ทธ๋žจ์ด ์•„๋‹Œ ์Šค์ƒท

 

 


 

 

๐Ÿ™‹‍โ™‚๏ธ๋‹ด๋‹น ํŒŒํŠธ

1. API : ์—ฌ์ • ๋ณต์ˆ˜ ๋“ฑ๋ก

2. ๋ฐœํ‘œํšŒ ์ž๋ฃŒ ์ค€๋น„ ๋ฐ ๋ฐœํ‘œ

 

1๏ธโƒฃ API : ์—ฌ์ • ๋ณต์ˆ˜ ๋“ฑ๋ก

POST /api/trips/{tripId}/itineraries

 

 

Controller

 

Service

 

 

 

Common utility class - Entity Dto Converter

 

์˜ˆ์™ธ์ฒ˜๋ฆฌ

 

 

ํ…Œ์ŠคํŠธ ์ฝ”๋“œ

 

ItineraryServiceTest.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
package kr.co.fastcampus.travel.service;
 
import static kr.co.fastcampus.travel.TravelTestUtils.createItinerary;
import static kr.co.fastcampus.travel.TravelTestUtils.createItineraryRequest;
import static kr.co.fastcampus.travel.TravelTestUtils.createLodgeRequest;
import static kr.co.fastcampus.travel.TravelTestUtils.createRoute;
import static kr.co.fastcampus.travel.TravelTestUtils.createRouteRequest;
import static kr.co.fastcampus.travel.TravelTestUtils.createStayRequest;
import static kr.co.fastcampus.travel.TravelTestUtils.createTrip;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
 
import java.util.List;
import java.util.Optional;
import java.util.stream.IntStream;
import kr.co.fastcampus.travel.common.exception.EntityNotFoundException;
import kr.co.fastcampus.travel.controller.request.ItineraryRequest;
import kr.co.fastcampus.travel.controller.request.LodgeRequest;
import kr.co.fastcampus.travel.controller.request.RouteRequest;
import kr.co.fastcampus.travel.controller.request.StayRequest;
import kr.co.fastcampus.travel.entity.Itinerary;
import kr.co.fastcampus.travel.entity.Route;
import kr.co.fastcampus.travel.entity.Trip;
import kr.co.fastcampus.travel.repository.ItineraryRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
 
@ExtendWith(MockitoExtension.class)
class ItineraryServiceTest {
 
    @Mock
    private ItineraryRepository itineraryRepository;
 
    @InjectMocks
    private ItineraryService itineraryService;
 
    @Test
    @DisplayName("์—ฌ์ • ๋ณต์ˆ˜ ๋“ฑ๋ก")
    void addItineraries() {
        // given
        Trip trip = createTrip();
        List<ItineraryRequest> requests = IntStream.range(03)
            .mapToObj(i -> createItineraryRequest())
            .toList();
 
        //when
        Trip returnedTrip = itineraryService.addItineraries(trip, requests);
 
        //then
        assertThat(returnedTrip).isNotNull();
        assertThat(returnedTrip.getItineraries().size()).isEqualTo(3);
    }
 
    @Test
    @DisplayName("์—ฌ์ • ๋ณต์ˆ˜ ๋“ฑ๋ก ์‹คํŒจ")
    void addItineraries_fail() {
        // given
        Trip trip = createTrip();
        List<ItineraryRequest> requests = IntStream.range(03)
            .mapToObj(i -> createItineraryRequest())
            .toList();
        Trip otherTrip = createTrip();
 
        //when
        Trip returnedTrip = itineraryService.addItineraries(otherTrip, requests);
 
        //then
        assertThat(trip).isNotEqualTo(returnedTrip);
        assertThat(trip.getItineraries().size()).isNotEqualTo(returnedTrip.getItineraries().size());
    }
}
 
cs

 

 

TravelControllerTest.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
package kr.co.fastcampus.travel.controller;
 
import static kr.co.fastcampus.travel.TravelTestUtils.createItinerary;
import static kr.co.fastcampus.travel.TravelTestUtils.createItineraryRequest;
import static kr.co.fastcampus.travel.TravelTestUtils.createLodgeRequest;
import static kr.co.fastcampus.travel.TravelTestUtils.createRouteRequest;
import static kr.co.fastcampus.travel.TravelTestUtils.createStayRequest;
import static kr.co.fastcampus.travel.TravelTestUtils.createTrip;
import static kr.co.fastcampus.travel.TravelTestUtils.putAndExtractResponse;
import static kr.co.fastcampus.travel.TravelTestUtils.requestDeleteApi;
import static kr.co.fastcampus.travel.TravelTestUtils.requestFindAllTripApi;
import static kr.co.fastcampus.travel.common.response.Status.FAIL;
import static kr.co.fastcampus.travel.common.response.Status.SUCCESS;
import static kr.co.fastcampus.travel.controller.util.TravelDtoConverter.toTripSummaryResponse;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.SoftAssertions.assertSoftly;
import static org.junit.jupiter.api.Assertions.assertAll;
 
import io.restassured.RestAssured;
import io.restassured.path.json.JsonPath;
import io.restassured.response.ExtractableResponse;
import io.restassured.response.Response;
import java.time.LocalDate;
import java.util.List;
import java.util.stream.IntStream;
import kr.co.fastcampus.travel.ApiTest;
import kr.co.fastcampus.travel.common.response.Status;
import kr.co.fastcampus.travel.controller.request.ItineraryRequest;
import kr.co.fastcampus.travel.controller.request.LodgeRequest;
import kr.co.fastcampus.travel.controller.request.RouteRequest;
import kr.co.fastcampus.travel.controller.request.StayRequest;
import kr.co.fastcampus.travel.controller.request.TripRequest;
import kr.co.fastcampus.travel.controller.response.ItineraryResponse;
import kr.co.fastcampus.travel.controller.response.TripResponse;
import kr.co.fastcampus.travel.controller.response.TripSummaryResponse;
import kr.co.fastcampus.travel.entity.Itinerary;
import kr.co.fastcampus.travel.entity.Trip;
import kr.co.fastcampus.travel.repository.ItineraryRepository;
import kr.co.fastcampus.travel.repository.TripRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
 
public class TravelControllerTest extends ApiTest {
 
    @Autowired
    private TripRepository tripRepository;
 
    @Autowired
    private ItineraryRepository itineraryRepository;
 
    @Test
    @DisplayName("์—ฌ์ • ๋ณต์ˆ˜ ๋“ฑ๋ก")
    void addItineraries() {
        // given
        Trip trip = createTrip();
        tripRepository.save(trip);
        String url = "/api/trips/1/itineraries";
        List<ItineraryRequest> request = IntStream.range(03)
            .mapToObj(i -> createItineraryRequest())
            .toList();
 
        //when
        ExtractableResponse<Response> response = RestAssured
            .given().log().all()
            .contentType(MediaType.APPLICATION_JSON_VALUE)
            .body(request)
            .when()
            .post(url)
            .then().log().all()
            .extract();
 
        // then
        JsonPath jsonPath = response.jsonPath();
        String status = jsonPath.getString("status");
        TripResponse data = jsonPath.getObject("data", TripResponse.class);
 
        assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value());
 
        assertSoftly((softly) -> {
            softly.assertThat(status).isEqualTo("SUCCESS");
            softly.assertThat(data.id()).isEqualTo(1);
            softly.assertThat(data.name()).isEqualTo("tripName");
            softly.assertThat(data.itineraries().size()).isEqualTo(3);
        });
    }
 
    private Trip saveTrip() {
        Trip trip = createTrip();
        return tripRepository.save(trip);
    }
 
    private Itinerary saveItinerary(Trip trip) {
        Itinerary itinerary = createItinerary(trip);
        return itineraryRepository.save(itinerary);
    }
}
 
cs

 

์‹œ์—ฐ ๊ฒฐ๊ณผ

 

 

2๏ธโƒฃ๋ฐœํ‘œํšŒ ์ž๋ฃŒ ์ค€๋น„ ๋ฐ ๋ฐœํ‘œ

 


 

๐Ÿ‘ฅํŒ€ํšŒ๊ณ 

 

ํ”„๋กœ์ ํŠธ๊ฐ€ ๋๋‚˜๊ณ  ๋‹ค๊ฐ™์ด ๋ชจ์—ฌ์„œ ํ”„๋กœ์ ํŠธ์— ๋Œ€ํ•œ ๋ฆฌ๋ทฐ ๋ฐ ํ”ผ๋“œ๋ฐฑ, ํšŒ๊ณ  ํ•˜๋Š” ์‹œ๊ฐ„์„ ๊ฐ€์กŒ๋‹ค.

1. ๊ฒ€์ฆ ์‹คํŒจ ์‹œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ์ง€์ • (์ˆœ์„œ๋ณด์žฅ๊นŒ์ง€)
2. ์—ฌํ–‰ ์ข…๋ฃŒ์ผ์ž๊ฐ€ ์—ฌํ–‰ ์‹œ์ž‘์ผ์ž๋ณด๋‹ค ์•ž์„œ๋Š”์ง€์— ๋Œ€ํ•œ ์˜ˆ์™ธ์ฒ˜๋ฆฌ
3. Postman ์š”์ฒญ๋“ค์— ๋Œ€ํ•œ ๊ณต์œ 
4. ZonedDateTime ์‚ฌ์šฉ
5. Create/Update ๋ณ„๋กœ DTO ๋‚˜๋ˆ„๊ธฐ
6. Controller์˜ DTO, Service์˜ DTO
7. Enum ์ด๋™์ˆ˜๋‹จ
8. ์‹œ์ž‘์ผ์‹œ/์ข…๋ฃŒ์ผ์‹œ๋ฅผ ๋‹ค๋ฃจ๋Š” Entity์™€ ๊ทธ ์•„๋ž˜์— ๋ชจ์•„์ ธ์žˆ๋Š” ๊ฒ€์ฆ ๋ฉ”์„œ๋“œ๋“ค (๋ณด๋ฅ˜)

 

์˜ˆ์™ธ์ฒ˜๋ฆฌ๋ฅผ ๋ณด๋‹ค ๋” ์‹ ๊ฒฝ์จ๋ณด์ž๋Š” ์ด์•ผ๊ธฐ๊ฐ€ ๋‚˜์™”๊ณ , RequestDto , ResponseDto ๋„ ์—ญํ• ๋ณ„๋กœ ์„ธ๋ถ„ํ™”ํ•˜์—ฌ ๋‚˜๋ˆ„์ž๋Š” ์ด์•ผ๊ธฐ๊ฐ€ ๋‚˜์™”๋‹ค. ๋˜ํ•œ ๊ณ„์ธต๋ณ„๋กœ ์“ฐ๋Š” DTO๋„ ๋‚˜๋ˆ„์–ด ๊ฒฐํ•ฉ๋„๋ฅผ ๋”์šฑ ๋‚ฎ์ถ”์ž๋Š” ์ด์•ผ๊ธฐ๋ฅผ ๋‚˜๋ˆด๋‹ค. PostMan ํ…Œ์ŠคํŠธ๋ฅผ ์ €์žฅํ•˜์—ฌ ํŒ€์›์ด importํ•˜์—ฌ  ๋‹ค๊ฐ™์ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜์ž๊ณ  ํ•˜๊ณ , ์ด๋™์ˆ˜๋‹จ์„ Enum์œผ๋กœ ๋„ฃ์ž๋Š” ์ด์•ผ๊ธฐ๋„ ๋‚˜์™”๋‹ค.

์ž˜ํ•œ ์ ์€ ๋‹ค๋ฅธ ์กฐ๋“ค์— ๋น„ํ•ด ๋”์šฑ ์ฝ”๋“œ๋ฅผ ํด๋ฆฐํ•˜๊ฒŒ ์งฐ๋‹ค๋Š” ์ ๊ณผ ๊ฐ์ž ๊ฐœ์ธ์ด ๋งก์€ API์˜ ํ…Œ์ŠคํŠธ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ–ˆ๋‹ค๋Š” ์ ์— ์žˆ๋‹ค.

 

 

๋Œ“๊ธ€