π ꡬν μ€λͺ
μ κ° μ¬μ©ν Java versionμ 11.0.2λ₯Ό μ¬μ©νμκ³ , Spring Boot λ 2.7.14 λ₯Ό μ¬μ©νμμ΅λλ€. μλ° 11μ κ²½μ° LTS λ²μ μ΄κ³ , μλ° 8λ³΄λ€ λ§μ κΈ°λ₯μ μ§μνμ¬ 11λ²μ μ μ ννμμ΅λλ€.
κ·Έλ¦¬κ³ DBμ κ²½μ°μλ κ°λ° κ³Όμ μμ H2 DBλ₯Ό μ¬μ©νμκ³ , CI/CD μ΄νμ AWS RDSμ MySQL DBλ₯Ό μ¬μ©νμμ΅λλ€.
DB μ€κ³ λ° API λ¬Έμ μμ±
- DB μ€κ³
- νμκ³Ό μ§λ¬Έμ κ²½μ° νμ 1λͺ μ΄ μ¬λ¬ κ°μ μ§λ¬Έμ μμ±νκ±°λ μμ μμ±νμ§ μμ μλ μκ³ , λ΅λ³μ κ²½μ°λ νμκ³Όμ κ΄κ³λ λ§μ°¬κ°μ§μ΄λ©° μ§λ¬Έ 1κ°μ λ΅λ³μ΄ μ¬λ¬ κ° λ¬λ¦΄ μ μμΌλ―λ‘ μμ κ°μ΄ μ€κ³νμμ΅λλ€. νκ·Έλ μ§λ¬Έμ μ¬λ¬ κ°κ° λ¬λ¦΄ μ μλ ννλ‘ μ€κ³νμμ΅λλ€. N:M κ΄κ³μΈ νκ·Έμ μ§λ¬Έμ κ΄κ³λ₯Ό 1:N 1:MμΌλ‘ λλκΈ° μν΄ μ§λ¬Ένκ·Έ(QuestionTag)λ₯Ό μμ±νμμ΅λλ€.
- νμλ€κ³Όμ μν΅μ μν΄μ λ Έμ νμ΄μ§μ λμ€μ½λλ‘ μν΅μ νλ©΄μ μμ μ νμμ΅λλ€. νλ‘μ νΈμ λ€μ΄κ°κΈ°μ μ¬μ΄νΈμ μꡬμ¬νμ νμ νκΈ° μν΄ μꡬμ¬ν λͺ μΈμλ₯Ό μμ±νμκ³ , κ·Έμ λ°λΌ μν μ λΆλ°°νμμ΅λλ€. λ¨Όμ νμλ€κ³Ό ν¨κ» ERD λ€μ΄μ΄κ·Έλ¨κ³Ό APIλͺ μΈμλ₯Ό μμ±νμμ΅λλ€.
- API λ¬Έμ[API λͺ μΈμ]
- μμ±μλ νμ λͺ¨λκ° μ°Έμ¬νμ¬ μμ±νμκ³ κ°μ λ³ΈμΈμ λλ©μΈμ μμ±νμμ΅λλ€. μ λ μ§λ¬Έ, λ΅λ³, νκ·Έμ API λ¬Έμλ₯Ό μμ±νμλλ° APIλ¬Έμκ° νλ‘ νΈμλμ μν΅νλ μλν¬μΈνΈλΌλ μ μ νλ² λ μ μ μμμ΅λλ€. μμ±μ μν΄ RESTfulν API μμ± λ°©λ²μ λ§μ΄ μ°Ύμ보μκ³ , μ΄λ€ μμΌλ‘ μμ±νλ κ²μ΄ μ’μ APIμΈμ§ μ μ μμμ΅λλ€.
- APIλ¬Έμλ Swaggerμ PostManμ κ³ λ―Όνμμ΅λλ€. μ ν¬λ ꡬνμ μμ λ¬Έμλ₯Ό μμ±νλ μκ°μ΄ μμκΈ°μ, κ°λ° μ½λλ₯Ό λ°νμΌλ‘ APIλ¬Έμλ₯Ό μμ±νλ Swagger λ³΄λ€ λ¬Έμλ₯Ό μμ± νμ νμ© κ°λ₯ν Postmanμ νμ©νμ¬ λ¬Έμλ₯Ό μμ±νμμ΅λλ€. λ¨Όμ λ¬Έμ μμ±μ νκ² λλ APIꡬμ±μ λΌλλ₯Ό μ‘μ μ μμκ³ , API ν΅μ μ ν μ€νΈ μ©λλ‘ Postmanμ μ¬μ©νκΈ° λλ¬Έμ μ κ·Όμ΄ νΈνμ΅λλ€.
μ§λ¬Έ, λ΅λ³, νκ·Έ CRUD ꡬν
- μ§λ¬Έ μ‘°νμ κΈ°λ₯
- μ§λ¬Έ κ²μκΈμ μ‘°νμ κΈ°λ₯μ μΆκ°νμμ΅λλ€. Get μμ²μΌλ‘ findQuestionμ μ κ·Όμ viewCountUp λ©μλμμ μ‘°νμμ μ¦κ°λ₯Ό ꡬννμμ΅λλ€. νμ§λ§ findQuestionμμ λ ν¬μ§ν 리μ saveκ° μΌμ΄λλ λ°©μμΌλ‘ ꡬνμ νμ¬μ μΆνμ λ°©μμ λ³κ²½ν΄λ³΄λ €κ³ ν©λλ€.
//QuestionService.java public Question findQuestion(Long questionsId) { Question question = questionRepository.findById(questionsId) .orElseThrow(() -> new BusinessLogicException(ExceptionCode.QUESTION_NOT_FOUND)); viewCountUp(question); // -> μ‘°νμ questionRepository.save(question); return question; } //μ‘°νμ μ¦κ°(get Question) private static void viewCountUp(Question question) { Long view = question.getViews(); question.setViews(++view); }
- TimeStamp ꡬν
- κ²μκΈκ³Ό, λ΅λ³, μ μ μ μμ±μ μμ΄μ μμ±μκ°κ³Ό μμ μκ°μ΄ 곡ν΅μ μΌλ‘ ꡬνμ΄ λμμ΅λλ€. μ€λ³΅μ μ κ±°νκΈ° μν΄μ TimeStamp ν΄λμ€λ₯Ό ꡬννμ¬ Entityμ μμνμ¬ μ¬μ©νλ λ°©μμ ννμ΅λλ€.
μμ κ°μ΄ Question.classμμ TimeStampλ₯Ό μμνμ¬ μ¬μ©νμμ΅λλ€. μμ¬μ΄ λΆλΆμ modifiedAtμ κ²½μ° LocalDateTimeμ μ§μ ν λΉν΄μ€¬λλ°, μμ μ‘°νμ ꡬν λΆλΆμμ Getμμ²μ΄ λ°μν λ JPA Auditing κΈ°λ₯μ΄ νμ±ν λμ΄, modifiedAtμ΄ λ°λλ μ΄μκ° μμ΄ μ μ©νμμ΅λλ€. μ΄λ μΆνμ λ€λ₯Έ λ°©λ²μΌλ‘ 리ν©ν°λ§νκ³ μΆμ΅λλ€.//Question.class @Entity @Getter @Setter public class Question extends TimeStamp { ... }
//TimeStamp.class @Getter @Setter @MappedSuperclass @EntityListeners(AuditingEntityListener.class) public class TimeStamp { @CreatedDate @Column(updatable = false) private LocalDateTime createdAt; private LocalDateTime modifiedAt = LocalDateTime.now(); }
- κ²μκΈκ³Ό, λ΅λ³, μ μ μ μμ±μ μμ΄μ μμ±μκ°κ³Ό μμ μκ°μ΄ 곡ν΅μ μΌλ‘ ꡬνμ΄ λμμ΅λλ€. μ€λ³΅μ μ κ±°νκΈ° μν΄μ TimeStamp ν΄λμ€λ₯Ό ꡬννμ¬ Entityμ μμνμ¬ μ¬μ©νλ λ°©μμ ννμ΅λλ€.
CI/CD (AWS EC2 + Github Action)
CI/CDλ μ²μ μ§ννλ€ λ³΄λ μ΄λ €μ΄ μ μ΄ λ§μμ΅λλ€. μλμ κ°μ λ°©μμΌλ‘ ꡬνμ νμμ΅λλ€. Jenkinsμ μ¬μ©μ κ³ λ €νμλλ° νλ‘μ νΈμ κ·λͺ¨λ μκ° λ±μ κ³ λ € νμ λ Github Actionμ΄ νλ‘μ νΈμ μ ν©νλ€κ³ νλ¨νμ¬ μ§ννμμ΅λλ€.
Github λ ν¬μ§ν 리μ mainμΌλ‘ pushνκ² λλ©΄ github Actionμ΄ μλνκ² λκ³ , AWSμ CodeDeployλ₯Ό ν΅ν΄ EC2μ λ°°ν¬μ μ€νμ΄ λλ λ°©μμΌλ‘ ꡬνμ νμμ΅λλ€.
name: CI-CD
on:
push:
branches:
- main
workflow_dispatch:
pull_request:
branches:
- main
env:
S3_BUCKET_NAME: pre016
RESOURCE_PATH: server/src/main/resources/application.yml
CODE_DEPLOY_APPLICATION_NAME: pre016-codedeploy-app
CODE_DEPLOY_DEPLOYMENT_GROUP_NAME: pre016-codedeploy-deployment-group
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up JDK 11
uses: actions/setup-java@v3
with:
java-version: 11
distribution: 'temurin'
# appspec.yml, scripts(start.sh, stop.sh)λλ ν 리 볡μ¬
- name: Copy appspec.yml to current directory
run: |
cp server/appspec.yml .
cp -r server/scripts .
shell: bash
- name: Build with Gradle and print build result
run: |
cd server
chmod +x gradlew
./gradlew build -x test
shell: bash
- name: Copy jar file to current directory
run: cp server/build/libs/*.jar .
shell: bash
- name: Make zip file
run: zip -r ./$GITHUB_SHA.zip .
shell: bash
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ secrets.AWS_REGION }}
- name: Upload to S3
run: |
aws deploy push \
--application-name ${{ env.CODE_DEPLOY_APPLICATION_NAME }} \
--ignore-hidden-files \
--s3-location s3://$S3_BUCKET_NAME/$GITHUB_SHA.zip \
--source .
- name: Code Deploy
run: |
aws deploy create-deployment \
--application-name ${{ env.CODE_DEPLOY_APPLICATION_NAME }} \
--deployment-config-name CodeDeployDefault.AllAtOnce \
--deployment-group-name ${{ env.CODE_DEPLOY_DEPLOYMENT_GROUP_NAME }} \
--s3-location bucket=$S3_BUCKET_NAME,key=$GITHUB_SHA.zip,bundleType=zip
CI/CD λ₯Ό ꡬννλ©΄μ Github Actionμ μ¬μ©ν΄λ³Ό μ μλ μ’μ κΈ°νμμ΅λλ€. CI/CDλ₯Ό ꡬννλ Flowκ° μ λ§ λ€μνλ€λ κ²μ μκ² λμκ³ κΈ°νκ° λλ€λ©΄ λ€λ₯Έ λ°©μμΌλ‘ ꡬνν΄ λ³΄κ³ μΆμμ΅λλ€. κ·Έλ¦¬κ³ ymlμ μμ±νλ λ°©λ²λ 곡λΆνμμ΅λλ€. ymlμ κ²½μ°μλ λ°°μ΄ λ±μ ννν μ μλ€λ κ²μ μμμ΅λλ€. μ΄μ κ΄λ ¨νμ¬ λΈλ‘κ·Έμ κΈ°λ‘ν΄ λμμ΅λλ€.
MySQL DBμλ² μ°λ(AWS RDS μ¬μ©)
κ°λ°λ¨κ³μμ H2 DBλ₯Ό μ¬μ©νλ€λ³΄λ, μλ²κ° μ¬κΈ°λ λλ©΄ λ°μ΄ν°κ° μ΄κΈ°ν λμμ΅λλ€. μ΄λ₯Ό μν΄ MySQLμ κ³ λ €νμμ΅λλ€. μ΄λ μ ν¬μκ² μ νν μ μλ λ°©λ²μ RDSλ₯Ό μ¬μ©νλ λ°©λ²κ³Ό EC2μ μ§μ MySQLμ μ€μΉνμ¬ μ΄μνλ λ°©λ²μ΄ μμμ΅λλ€. μ λ RDSλ₯Ό νλ² κ²½νν΄λ³΄κ³ μΆμμ΅λλ€. κ·Έλμ RDSλ₯Ό μ ννμκ³ , RDSλ₯Ό μ¬μ©μμ μ»κ² λλ μ₯μ μ λν΄ κ³΅λΆν μ μμμ΅λλ€. νμ§λ§ RDSμ¬μ©νκ² λλ λΉμ©μ λν λΆλ΄μ΄ μκ²Όμ΅λλ€. κ·Έλμ νμ¬λ RDS DBλ₯Ό λ΄λ €λμ μνμ΄κ³ , EC2μ μ§μ μ€μΉνμ¬ λ¦¬ν©ν λ§ μμ μ μ¬μ© ν κ³νμ μΈμ°κ³ μμ΅λλ€.
π‘ μ΄λ €μ λ μ / λ°°μ΄ μ
MappedBy μλ¬
Spring Data JPAμ mappedByλ₯Ό μμ±νλ κ²μ μ΄λ €μμ΄ μμμ΅λλ€. μ΄λ₯Ό ν΄κ²°νκΈ° μν΄μ ν μ΄λΈμ μ°κ΄κ΄κ³μ λν 곡λΆλ₯Ό νμκ³ , μλ°©ν₯ 맀νμμ λμ€ νλκ° μΈλν€λ₯Ό κ΄λ¦¬ν΄μΌνκ³ μ΄λ μ°κ΄κ΄κ³μ μ£ΌμΈ(Owner)μ΄ ν΄μΌνλ€λ κ²μ μμμ΅λλ€. μ΄λ Ownerλ mappedBy μμ±μ μ¬μ©νμ§ μκ³ , Ownerκ° μλλ©΄ mappedByμμ±μ μ¬μ©ν©λλ€.
μνμ°Έμ‘° μλ¬
@Entity
@Getter
@Setter
public class Question extends TimeStamp {
...
@JsonIgnore //μνμ°Έμ‘° λ°μνμ¬ stackoverflow μλ¬λ¨ > JsonIgnore μ¬μ©ν΄μ μμ μ€λ€
@OneToMany(mappedBy = "question", cascade = CascadeType.ALL)
private List<Answer> answers = new ArrayList<>();
@JsonIgnore //μνμ°Έμ‘° λ°μνμ¬ stackoverflow μλ¬λ¨ > JsonIgnore μ¬μ©ν΄μ μμ μ€λ€
@OneToMany(mappedBy = "question", cascade = CascadeType.ALL)
private List<QuestionTag> questionTags = new ArrayList<>();
}
OneToMany κ΄κ³μμ Getμμ²μΌλ‘ μ‘°νμμ μνμ°Έμ‘°μλ¬κ° λ°μνμμ΅λλ€. μ΄λ₯Ό λ°©μ§νκΈ° μν΄μ @JsonIgnoreλ₯Ό μ¬μ©νμ¬ λ°©μ§ν΄μ€¬μ΅λλ€.
H2 DB μμ½μ΄ μλ¬
μ΅μ΄μ ν μ΄λΈμ€κ³μμ νμμ Userλ‘ μ€κ³νμμ΅λλ€. μ΄λ, κ°λ°κ³Όμ μμ μ¬μ©νλ H2 DBμ μμ½μ΄μ μΆ©λνλ λ¬Έμ κ° λ°μνμμ΅λλ€.
Caused by: org.h2.jdbc.JdbcSQLSyntaxErrorException: Syntax error in SQL statement "insert into [*]user
(id, created_at, deleted_at, last_modified_at, password, role, user_name) values
(default, ?, ?, ?, ?, ?, ?)"; expected "identifier"; SQL statement:
insert into user (id, created_at, deleted_at, last_modified_at, password, role, user_name) values (default, ?, ?, ?, ?, ?, ?) [42001-214]
μμ κ°μ μλ¬κ° λ°μνμκ³ , μ΄λ₯Ό ν΄κ²°νκΈ° μν΄μ User → Usersλ‘ ν μ΄λΈλͺ μ λ³κ²½νμμ΅λλ€.
@Tableμ΄λ Έν μ΄μ μ μ μ©νμ¬ λ³κ²½νμμ΅λλ€. μ΄λ₯Ό ν΅ν΄ ν μ΄λΈμ μ€κ³ ν λ μ¬λ°λ₯Έ λͺ μΉμ μ νλ κ²μ΄ μΌλ§λ μ€μνμ§ μ μ μμμ΅λλ€.
Validation μλ¬ [λΈλ‘κ·Έ μ 리 보기]
@NotBlank
private long password;
μμ κ°μ΄ μμ±νλλ μλ¬κ° λ°μνμμ΅λλ€. μ΄μ λ longνμ , intνμ λ±μ μμνμ μ λν΄μλ @NotBlank μ λν μ΄μ μ μ¬μ©ν μμκ³ λ§μ½ μ ν¨μ±κ²μ¬κ° νμνλ€λ©΄ @Min, @Maxλ±μ μ΅μ, μ΅λκ°μ μ§μ νλ λ°©μμΌλ‘ μ¬μ©μ΄ κ°λ₯ν©λλ€. μ΄λ₯Ό ν΅ν΄ μ¬μν λΆλΆμμλ λ¬Έμ κ° λ°μν μ μλ€λ κ²μ μμμ΅λλ€. Dtoλ₯Ό μμ±ν λ λΆμ¬λ£κΈ°λ₯Ό νμλλ° μ’ λ μ κ²½μ μ¨μΌκ² λ€κ³ μκ°νμμ΅λλ€.
CORS μλ¬
νλ‘ νΈμ μ°λνμ¬ ν μ€νΈλ₯Ό μ§ννλ κ³Όμ μμ μ λ§ λ§μ corsμλ¬κ° λ°μνμμ΅λλ€.
setAllowedOrigins μ setcredential ν¨κ» μ¬μ© λͺ» νλ κ²μ μμκ³ , μ΄λ₯Ό μ μ©νμ¬ corsFilter λ£μ΄μ testλ μ±κ³΅νμμ΅λλ€. μ΄μΈμλ κ±°μ λͺ¨λ ν μ€νΈ μ§ν μμ Corsμλ¬κ° λ°μνμλλ°, λ°±μλλ μ²μ νμ μ μ§ννλ μν©μ΄μκ³ , νλ‘ νΈλ μ²μ νμ μ μ§ν νλ€ λ³΄λ μ΄λκ° λ¬Έμ μΈμ§ μ νν νμ ν μκ° μμ΄μ μΌμ΄λλ λ¬Έμ κ° λλΆλΆ μ΄μμ΅λλ€. μ΄λ₯Ό ν΄κ²°νκΈ° μν΄μ νλ‘ νΈμ κ±°μ λ μ΄ μλλ‘ νλμ© ν μ€νΈλ₯Ό μ§ν νμκ³ κ²°κ΅ λ¬Έμ λ₯Ό ν΄κ²°ν μμμμ΅λλ€. μ΄λ₯Ό ν΅ν΄μ μ΄λ³΄ κ°λ°μλ€μ΄ κ°μ₯ μ λ₯Ό λ¨Ήλλ€λ Corsμ λν΄μ νλ² λ μκ² λμκ³ , μ΄λ€ μμΌλ‘ ν΄κ²°ν΄μΌ νλμ§, κ·Έλ¦¬κ³ μ΄λ λΆλΆμ μ§μ€ν΄μ νμΈ ν΄μΌ νλμ§ μ μ μμμ΅λλ€. νΉν preflight μμ²μ λν΄μ μμΈν 곡λΆν μ μμμ΅λλ€.