Make zip file에서는 빌드한 파일을 압축하는 작업을 실행한다. S3 버킷에 파일을 올리기 위해서는 해당 작업이 선행되어야 한다.
이때 $GITHUB_SHA는 커밋의 해쉬값으로 해당 작업에서 workflow가 진행하는 커밋의 해쉬값을 파일명으로 하는 압축파일을 만든다는것을 의미한다.
다음으로 Configure AWS credentials는 IAM에서 추가한 사용자의 액세스 키 정보를 통해 S3에 접근권한을 받아오기 위한 작업이다.
이때 ${{SECRETS.~}}는 github의 해당 리포지토리의 setting에서 Secrets설정한 값을 읽어 온다.
따라서 해당 작업을 위해서는 github에 IAM 사용자 추가에서 나온 액세스 키 정보들을 등록해야한다.
다음 설정들을 모두 끝낸후 Github Actions를 돌리면 S3 버킷에 zip파일이 담긴것을 AWS console를 통해 확인할 수 있다.
다음으로 CodeDeploy 설정을 해야한다.
CodeDeploy를 작동시키기 위해서는 먼저 Github Actions이 참조하는 workflow의 main.yml파일에 CodeDeploy를 통해 배포하기 위한 명령어를 추가하고,
프로젝트 최상단에 AppSpec.yml파일을 추가한다.
AppSpec.yml은 각 배포단계에서 어떤 스크립트를 실행시킬지 적어놓은 명세서라고 할 수 있다.
GithubActions는 main.yml에 추가한 명령어를 통해 AWS의 CodeDeploy에 배포 요청을 보내고
CodeDeploy는 EC2에 설치되어있는 CodeDeployAgent에게 배포 명령을 내린다.
배포명령을 받은 Agent들은 깃허브에 저장되있는 프로젝트 전체를 서버에 내려받고 appspec.yml파일을 읽어 알맞은 스크립트를 실행시켜 배포를 진행한다.
CodeDeployAgent는 배포 성공 여부를 CodeDeploy에 알려주기 때문에 AWS console의 CodeDeploy에서 배포 성공여부를 확인할 수 있다.
이제 본격적으로 CodeDeploy설정을 해보겠다.
먼저 EC2에 CodeDeployAgent를 설치하여한다. EC2에 자바를 설치되어있다고 가정하고 다음 명령어들을 치면 된다.
# 패키지 매니저 업데이트, ruby설치
sudo apt-get update
sudo apt-get install ruby
sudo apt-get install wget
# 만약 서울리전에 있는 CodeDeploy 리소스 키트 파일을 다운받고 싶다면
# wget https://aws-codedeploy-ap-northeast-2.s3.ap-northeast-2.amazonaws.com/latest/install
cd /home/ec2-user
wget https://bucket-name.s3.region-identifier.amazonaws.com/latest/install
# 해당 파일에 실행 권한 부여
chmod +x ./install
# 설치 진행 및 Agnet 상태확인
sudo ./install auto
sudo service codedeploy-agent status
공식문서에서는 CodeDeployAgent를 설치하는 과정에서 apt-get대신 yum를 사용하는데 이것은 Linux가 어떤 계열인지에 따라 달라진다.
현재 사용중인 Linux가 레드헷 계열이라면 yum을 사용하고 우분투, 데비안계열이라면 apt-get를 사용해야한다.
나는 우분투 계열의 Linux를 사용중이기 때문에 yum대신 apt-get를 사용하였다.
이제는 EC2가 S3와 CodeDeploy에 대한 접근권한을 얻기 위해 IAM역할을 부여해야 한다.
위에서 GithubActions가 AWS의 S3에 접근하기위해 IAM사용자를 추가했었다.
IAM에대해 부연설명 하자면, GithubActions같이 외부에서 AWS의 접근 권한을 얻기 위해서는 IAM사용자를 추가해야하고,
EC2처럼 AWS에서 제공하는 서비스가 AWS내부 서비스에 접근하기 위해서는 IAM역할을 추가해줘야 한다.
역할 만들기에 들어가 사용 사례에서 EC2를 선택하고, AmazonS3FullAccess와 AWSCodeDeployFullAccess 정책을 추가한다.
역할을 성공적으로 생성했다면 EC2대시보드에 들어가 해당 EC2에 생성한 IAM역할을 부여한다.
다음으로 CodeDeploy에도 IAM역할을 생성해서 부여해야 한다.
IAM 역할만들기에서 CodeDeploy를 선택하고 사용사례에서 CodeDeploy를 선택하여 넘어간다.
이제 CodeDeploy 애플리케이션을 생성해야한다.
AWS console에서 CodeDeploy-애플리케이션-애플리케이션생성 에서 EC2를 선택하고 진행한다.
배포그룹 생성에서는 서비스역할에는 방금전 생성한 CodeDeploy용 IAM역할을 입력하고 배포유형은 현재 위치를 선택한다.
관계형 데이터베이스는 상속관계가 따로있지는 않지만 데이터베이스의 슈퍼타입 서브타입 관계라는 모델링 기법이 객체 상속과 유사하다.
상속관계 매핑이란 객체의 상속 구조와 DB의 슈퍼타입 서브타입관계를 매핑하는것을 말한다.
상속관계 매핑에는 3가지 방법이 존재한다.
각각 테이블로 변환 -> 조인 전략
통합 테이블로 변환 -> 단일 테이블 전략
서브타입 테이블로 변환 -> 구현 클래스마다 테이블 전략
먼저 조인 전략을 다이어그램과 코드로 표현하면 다음과 같다.
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn // 서브타입을 나타내는 속성값이 테이블에 추가됨
public class Item {
@Id @GeneratedValue
private Long id;
private String name;
private int price;
}
@Entity
public class Album extends Item {
private String artist;
}
@Entity
public class Movie extends Item {
private String director;
private String actor;
}
@Entity
public class Book extends Item {
private String author;
private int isbn;
}
하지만 조회시 조인을 많이 사용하여 성능이 저하되고, 조회 쿼리가 복잡하며, 데이터 저장시 insert 쿼리가 2번 호출된다.
다음으로 단일 테이블 전략이 있다.
단일 테이블 전략은 자식 엔티티의 모든 속성들을 부모 테이블에 몰아넣는 방식으로 상속관계를 구현한다.
단일 테이블 전략을 표현하는 다이어그램과 코드는 다음과 같다.
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn // 없어도 DTYPE속성 생김
public class Item {
@Id @GeneratedValue
private Long id;
private String name;
private int price;
}
@Entity
public class Album extends Item {
private String artist;
}
@Entity
public class Movie extends Item {
private String director;
private String actor;
}
@Entity
public class Book extends Item {
private String author;
private int isbn;
}
단일 테이블 전략은 조인이 필요 없으므로 일반적으로 조회 성능이 빠르고 조회 쿼리 또한 단순하다.
반면 자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야 하며, 단일 테이블이 커져 조회성능이 오히려 느려질 수 있다.
마지막으로 구현 클래스마다 테이블 전략이 있는데, 여러 자식 테이블을 함께 조회할 때 성능이 느리고 자식 테이블을 통합해서 쿼리하기 어렵다.
이러한 단점들로 인해 해당 전략은 데이터베이스 설계자와 ORM 전문가 양쪽다 사용하기 꺼려하므로 대부분의 경우 사용하지 않는것이 바람직하다.
다음으로 설명할것은 @MappedSuperclass이다.
@MappedSuperclass는 공통 매핑 정보가 필요할 때 사용된다.
예를들어 대부분의 엔티티에 해당 객체가 만들어진 날짜(craetedDate)와 마지막 수정 날짜(lastModifiedBy)가 공통적으로 들어간다고 해보자.
이 경우 엔티티 하나하나마다 공통 필드를 추가하는것보다 추상클래스를 만들어 상속하는 식으로 구현하는것이 훨씬 효율적이다.
여기서 추상클래스 역할을 하는것에 @MappedSuperclass를 사용하면 된다.
@MappedSuperclass를 구현하는 코드는 다음과 같다.
@MappedSuperclass
public abstract class BaseEntity {
private LocalDateTime createdDate;
private LocalDateTime lastModifiedDate;
... // get set 구현
}
@Entity
public class otherEntity extends BaseEntity {
...
}
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
private String username;
}
@Entity
public class Team{
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
}
여기서 Team 객체를 통해서도 Member와의 관계를 알고 싶으면 양방향 연관관계를 걸어주면 된다.
다대일 양방향 연관관계를 나타내는 다이어그램과 코드는 다음과 같다.
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
private String username;
}
@Entity
public class Team{
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
private String name;
}
외래키가 있는 쪽이 연관관계의 주인이므로 Member가 연관관계의 주인이 된다.
코드를 보면 다대일 단방향 연관관계에서 Team에 @OneToMany로 member를 노드로 하는 List를 추가하기만 하면 되는것을 알 수 있다.
여기서 @OneToMany에 mappedBy로 연관관계의 주인인 Member의 team를 지정해 주면 된다.
다음으로 일대다 연관관계가 있다.
일대다는 일과 다 중에 일이 연관관계의 주인일때를 말한다. 하지만 테이블에서는 일대다 관계에서 항상 다 쪽에 외래키가 있다.
즉 일대다 연관관계에서는 연관관계의 주인이 반대편 테이블의 외래키를 관리하는 특이한 구조를 갖게 된다.
이때, 주의할 점은 @OneToMany를 사용할때 @JoinColumn을 꼭 사용해야 한다는 것이다. 그렇지 않으면 조인 테이블 방식이 디폴트로 적용되는데 이것은 중간에 테이블을 하나 추가하는 방식으로써 의도치 않은 테이블의 생성으로 인한 테이블 관리의 불편함이 생길 수 있다.
일대다 단방향 연관관계를 나타내는 다이어그램과 코드는 다음과 같다.
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
}
@Entity
public class Team{
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
@OneToMany
@JoinColumn(name = "TEAM_ID")
private List<Member> members = new ArrayList<>();
private String name;
}
일대다 단방향 매핑은 엔티티가 관리하는 외래키가 다른 테이블에 있어서 jpa를 통해 테이블을 다루는거 자체가 쉽지않다.
예를들어 team.setMembers같은 메서드를 쓸때 TEAM 테이블이 아닌 MEMBER 테이블에 update쿼리가 나가는등 코드의 일부분만 봐서는 쿼리 예측이 쉽지 않게 된다.
따라서 대다수의 경우에 일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하는게 바람직하다.
일대다 양방향 매핑은 공식적으로 존재하지 않지만 @JoinColumn(insertable = false, updatable = false)를 이용하여 일대다 양방향 매핑을 구현할 수 있다.
일대다 양방향 매핑을 나타내는 다이어그램과 코드는 다음과 같다.
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@ManyToOne
@JoinColumn(name = "TEAM_ID", insertable = false, updatable = false)
private Team team;
private String username;
}
@Entity
public class Team{
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
@OneToMany
@JoinColumn(name = "TEAM_ID")
private List<Member> members = new ArrayList<>();
private String name;
}
하지만 일대다 양방향 매핑 또한 대부분의 경우 다대일 양방향을 사용하여 해결할 수 있으므로 관리가 비교적 어려운 일대다 매핑보다는
구현이 용이하고 관리가 직관적인 다대일 양방향 매핑을 사용하는것이 바람직하다.
다음으로 일대일 연관관계가 있다.
일대일 연관관계에서는 주 테이블이나 대상 테이블 중에 외래키를 어디다 둘지 선택 가능하다.
예를들어 한명(MEMBER)당 최대 하나의 사물함(LOCKER)을 배정 받을 수 있다 했을때 MEMBER 테이블을 주 테이블, LOCKER테이블을 대상 테이블이라고 하자.
주 테이블에 외래키가 있는 단방향 매핑을 다이어그램과 코드로 보이면 다음과 같다.
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@OneToOne
@JoinColumn(name = "LOCKER_ID")
private Locker locker;
private String username;
}
@Entity
public class Locker {
@Id @GeneratedValue
@Column(name = "LOCKER_ID")
private Long id;
private String name;
}
보다시피 다대일 단방향 매핑과 매우 유사한것을 알 수 있다. 때문에 여기서 양방향 매핑을 거는것 또한 다대일 양방향 매핑과 매우 유사하다.
일대일 연관관계에서 주 테이블에 외래키가 있는경우 양방향 매핑에 대한 다이어그램과 코드는 다음과 같다.
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@OneToOne
@JoinColumn(name = "LOCKER_ID")
private Locker locker;
private String username;
}
@Entity
public class Locker {
@Id @GeneratedValue
@Column(name = "LOCKER_ID")
private Long id;
@OneToOne(mappedBy = "locker")
private Member member;
private String name;
}
지금까지는 일대일 연관관계중에서도 주 테이블에 외래키가 있는 경우를 살펴보았다.
다음으로 대상 테이블에 외래키가 있는경우에 대해 말하고자 한다.
먼저 대상 테이블(LOCKER)에 외래키가 있는 경우 Member(Member.locker)를 연관관계의 주인으로 일대일 단방향 매핑을 하는 방법은 없다.
대상 테이블(LOCKER)에 외래키가 있는 경우 양방향 매핑은 가능하다.
단, 연관관계의 주인 또한 LOCKER(Locker.member)로 설정 해야하고, 따라서 Member는 읽기만 가능하다.
일대일 관계에서 주 테이블에 외래키가 있는경우 다음과 같은 특징이 있다.
주 객체가 대상 객체의 참조를 가지는것 처럼 주 테이블에 외래키를 두고 대상 테이블을 찾는다.
JPA 매핑이 편리하고 주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인 가능하므로 객체지향 개발자가 선호한다.
단점으로는 값이 없으면 외래키에 null값을 허용해야 한다.
일대일 관계에서 대상 테이블에 외래키가 있는경우 다음과 같은 특징이 있다.
대상 테이블에 외래키가 존재한다.
주 테이블과 대상 테이블을 일대일에서 일대다 관계로 변형할 때 테이블 구조를 유지할수 있어 전통적인 데이터베이스 개발자가 선호한다.
단점으로는 프록시 기능의 한계로 지연로딩으로 설정해줘도 즉시로딩이 불가피하다.
마지막으로 다대다 연관관계가 있다.
JPA에서 @ManyToMany를 사용한 다대다 매핑을 지원하지만 해당 관계에관한 다른 속성값을 추가할 수 없는 문제가 있다.
따라서 다대다 연관관계는 연결 테이블용 엔티티를 추가해서 다대일 일대다 관계로 풀어서 구현하는게 일반적이다.
예를 들어 고객(Member)과 상품(Product)이 다대다 관계라고 했을때, 고객과 상품사이에 MemberProduct라는 엔티티를 추가하여 각각 일대다 다대일 매핑을한다.
다대다 관계를 일대다 다대일 관계로 풀어서 매핑한 다이어그램과 코드는 다음과 같다.
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@OneToMany(mappedBy = "member")
private List<MemberProduct> memberProduct = new ArrayList<>();
private String username;
}
@Entity
public class Product{
@Id @GeneratedValue
private Long id;
@OneToMany(mappedBy = "product")
private List<MemberProduct> memberProduct = new ArrayList<>();
private String name;
}
@Entity
public class MemberProduct {
@Id @GeneratedValue
private Long id;
@ManyToOne
@JoinColumn(name = "MEMBER_ID")
private Member member;
@ManyToOne
@JoinColumn(name = "PRODUCT_ID")
private Product product;
private int orderAmount;
private int orderDate
}