Docker Multi Stage build

: 도커 멀티 스테이지 빌드란 하나의 Dockerfile 안에서 빌드(컴파일) 환경과 실행 환경을 분리하여 최종 이미지를 최적화하는 기술입니다.

 

작동 방식

: 하나의 Dockerfile 안에 FROM 명령어를 여러 번 사용하여 여러 스테이지를 구성합니다.

  1. 빌드 스테이지 (작업실 ): 첫 번째 FROM문으로 시작하며, 소스 코드를 컴파일하는 데 필요한 모든 도구(컴파일러, SDK 등)가 설치된 무거운 이미지를 사용합니다. 여기서 애플리케이션을 빌드하여 실행 파일을 만듭니다.
  2. 최종 스테이지 (배송 상자 ): 두 번째 FROM문으로 시작하며, scratch나 alpine 같은 초경량 이미지를 기반으로 합니다. 그런 다음 COPY --from=[빌드 스테이지 이름] 명령을 사용하여 첫 번째 스테이지에서 만들어진 실행 파일만 복사해 옵니다.

이렇게 하면 빌드에만 필요했던 무거운 도구와 라이브러리는 최종 이미지에 포함되지 않습니다.

# 예시 Dockerfile

# stage 1 : 빌드 환경
FROM ubuntu:22.04 AS build-image
RUN apt-get update && apt-get install -y gcc
COPY src/hello.c /tmp
WORKDIR /tmp
RUN gcc -o hello-world hello.c
CMD ["/tmp/hello-world"]

# stage 2 : 이미지 생성
FROM scratch AS runtime-image
COPY --from=build-image /tmp/hello-world  .
CMD ["./hello-world"]

빌드중..

Stage 1: "작업실" - 빌드 환경

- 목적: 소프트웨어를 컴파일하거나 빌드하기 위한 모든 도구를 갖춘 환경입니다.

- 베이스 이미지: ubuntu, debian, golang, node 등 필요한 컴파일러, 라이브러리, 빌드 도구가 포함된 무거운 전체 운영체제를 사용합니다.

- 역할: 이 스테이지는 오직 최종 실행 파일을 만들어 내는 역할만 하고, 빌드가 끝나면 이 환경 전체는 버려집니다.

 

Stage 2: "제품" - 최종 실행 환경

- 목적: 빌드된 애플리케이션을 '실행'만 시키기 위한 최소한의 환경입니다.

- 베이스 이미지: scratch (완전히 비어있는 이미지), alpine (초경량 리눅스), 또는 distroless (언어 런타임만 포함된 이미지)처럼 극도로 가벼운 이미지를 사용합니다.

- 역할: COPY --from=[Stage 1 이름] 명령을 사용해 "작업실"에서 만들어진 최종 결과물(실행 파일)만 가져와 담습니다. 작업실에 있던 무거운 도구들이나 소스 코드는 일절 포함하지 않습니다.


도커 Multi Stage build를 왜 할까?

- 최종 이미지 크기 최소화: 최종 이미지는 수백 MB가 아닌 수 MB 수준으로 극단적으로 작아집니다.

- 보안 강화: 셸(shell), 패키지 매니저(apt, yum) 등 불필요한 도구가 전혀 없으므로 공격 표면(Attack Surface)이 획기적으로 줄어듭니다.

- 배포 및 관리 효율성: 이미지가 작아 레지스트리에 푸시하고 클러스터에서 풀(pull)하는 속도가 매우 빨라집니다.

 

* 공격 표면(Attack Surface)이 획기적으로 줄어듭니다.

 :   해커가 공격할 수 있는 방법이나 통로의 수를 줄인다는 의미입니다. Docker 이미지에서 불필요한 프로그램(셸, 네트워크 도구 등)을 모두 제거하면, 해커가 이용할 수 있는 보안 취약점(문과 창문) 자체가 사라져서 훨씬 안전해집니다.

 

 

반응형

 

 

안녕하세요

저번에 Fetch조인과 일반 조인 차이점에 대해 포스팅했었는데

그 궁금증의 시작

"왜 Fetch Join 대상에 alias를 사용하면 안될까?"

대해 포스팅 해보겠습니다.

 

 

일단 이유는 JPA의 엔티티 객체 그래프는 DB와 데이터 일관성을 유지해야 하기 때문입니다.

뭔 소리냐고요?

(저도 처음 듣고 엥스바리 했습니다.)

 

예시를 들어 보겠습니다.

(저는 이번에 공부하면서 영속성 컨텍스트라는 개념이 얼마나 중요한지 ㅋ

싸악 알게 되었습니다)


먼저 예시 데이터를 보겠습니다.

 

Team1 -  유저1, 유저2

Team2 - 유저3

인 상황입니다.

 

 

 

 

 

 

JPQL

// 1. 팀을 페치조회하는데 유저이름으로 필터링
public List<Team> findByMemberName(String memberNm){
    return em.createQuery("select t from Team t join fetch t.members m"+
            " where m.name = memberNm", Team.class)
        .setParameter("memberNm",memberNm)
        .getResultList();
}
// 2. teamId로 team을 가져옴
public List<Team> findByTeamId(Long teamId){
    return em.createQuery("select t from Team t where t.id =teamId", Team.class)
        .setParameter("teamId", teamId)
        .getResultList();
}

 

첫 번째 JPQL을 보면 Team을 조회하면서 fetch조인으로 연관된 Member 엔티티를 모두 가져오는데 별칭을 사용해 memberNm으로 필터링해서 가져옵니다. (Team1 {members=['유저1']} )

 

두 번째 JPQL은 단순히 teamId로 Team을 가져옵니다.

 

그럼 이 테스트 코드를 보겠습니다.

 

테스트 코드

@Test
public void 페치조인_데이터_일관성_확인() throws Exception{
    List<Team> teamsWithUser1 = memberRepository.findByMemberName("유저1");
    List<Team> fullTeamById = memberRepository.findByTeamId(1L);

   for (Team team:teamsWithUser1) {
       for (Member member:team.getMembers()) {
           System.out.println("teamsWithUser1.member = " + member.getName());
       }
    }
    for (Team team2:fullTeamById) {
        for (Member member:team2.getMembers()) {
            System.out.println("fullTeamById.member = " + member.getName());
        }
    }
}

 

단순히 위에 JPQL을 호출해서 해당 팀에 멤버를 조회하는 테스트 코드입니다.

 

결과가 예상 가능하신가요?

 

첫 번째 teamWithUser1에서는 유저1 / 두 번째 fullTeamById 에서는 teamId 1인 팀에 멤버를 가져오니까 유저1, 유저2 이렇게 조회될까요?

 

테스트 결과

    select
        team0_.team_id as team_id1_7_0_,
        members1_.member_id as member_i1_4_1_,
        team0_.name as name2_7_0_,
        members1_.city as city2_4_1_,
        members1_.street as street3_4_1_,
        members1_.zipcode as zipcode4_4_1_,
        members1_.name as name5_4_1_,
        members1_.team_team_id as team_tea6_4_1_,
        members1_.team_team_id as team_tea6_4_0__,
        members1_.member_id as member_i1_4_0__ 
    from
        team team0_ 
    inner join
        member members1_ 
            on team0_.team_id=members1_.team_team_id 
    where
        members1_.name='유저1'
2025-05-13 15:30:15.556 DEBUG 34196 --- [    Test worker] org.hibernate.SQL                        : 
    select
        team0_.team_id as team_id1_7_,
        team0_.name as name2_7_ 
    from
        team team0_ 
    where
        team0_.team_id='1'
team.member = 유저1
team2.member = 유저1

 

놀랍게도 teamId 1인 Team을 조회한 Team1의 멤버는 유저1만 나옵니다.

 

 왜일까요????

 

영속성 컨텍스트를 기억하시나요?

 

 

조회된 엔티티는 영속성 컨텍스트 1차 캐시에 들어가게 되고 식별자는 id값입니다.

그래서 처음 조회한 필터링된 Team1이 1차 캐시에

id = 1 , Entity = Team {Memebers=['유저1']} 이러한 형태로 들어가게 됩니다.

 

그다음 teamId 값으로 Team을 조회했을 때 db에 접근하기 전에 1차 캐시를 먼저 들려 id를 확인해서 있으니

기대값과는 다른 멤버를 반환하게 됩니다.

 

이렇게 데이터 일관성이 깨지게 됩니다.

 

그래서 페치 조인은 연관 엔티티를 모두를 가져오는 기능에 맞춰서 사용해야 됩니다.

 

 

 

 

정말 이거 공부하면서 좀 개념 틀이 잡힌 느낌이 들었습니다 ㅋ

여하튼 틀린 부분이나 궁금한 부분 있으시면 댓글 주시길 바랍니다~!

감사합니다~!

 

 

 

반응형

 

jpa활용2 강의를 듣고 있는데 김영한 강사님이 강의 중에 이런 말을 하셨음.

"fetch조인에서는 엘리어스를 쓰면 안된다.. .. .. "

그래서 왜지? 하고 생각만 했다가 완강하고 다시 검색해보는데.

 

https://www.inflearn.com/community/questions/15876/fetch-join-%EC%8B%9C-%EB%B3%84%EC%B9%AD%EA%B4%80%EB%A0%A8-%EC%A7%88%EB%AC%B8%EC%9E%85%EB%8B%88%EB%8B%A4

 

역시 나와 같은 사람이 있었음 ㅋ

그래서 답변을 쭉쭉 읽는데도 이해가 안 되어서 왜 안되나 했는데

ㅋㅋ..

 

내가 fetch 조인을 잘 모르고 있었음 ㅋ

그냥 단순히 fetch 조인 쓰면 연관관계에 있는 거 다 가져오는 거구만 개꿀 ㅋ

이 정도로만 생각해서 이해를 못 했던 것임

 

 

"JPA의 엔티티 객체 그래프는 DB와 데이터 일관성을 유지해야 하기 때문입니다.."

왜 사용하면 안되는지에 대한 영한님의 답변인데

<- 왜? 이것 또한 이해 못 했음..

그냥 가져오는 건 상관없는 거 아닌가 쩝 ..

이 생각했는데 fetch 조인은 엔티티를 가져오는 거 기 때문에.. 안됨. 큰일 남;;

이건 다음에 다른 글로 포스팅하고 먼저 이해해야 하는 일반 조인과 fatch 조인의 차이를 알아보겠다!

 


Fetch Join

일단 fetch 조인이란 sql과 비슷해 보이지만 jpql에서만 제공하는 기능으로 엔티티를 조회해 올 때 연관된 엔티티를

한 번에 같이 조회 할수 있는 기능이다. 

연관된 엔티티를 한번에 같이 조회할 수 있는 게 어떤 의미인가 하면! 함께 조회한 엔티티가 영속성 컨텍스트에 올라가게

되는데 밑에 예제를 보면서 자세히 보겠습니다 ㅋ

 

 

모든 멤버와 해당 멤버의 팀까지 조회해보겠습니다.

현재 데이터는 아래와 같습니다. 유저 1,2가 있고 모두 팀이 1인 상황입니다.

멤버, 팀 데이터

JPQL

// 1.일반조인
  public List<Member> findAll(){
        return em.createQuery("select m from Member m join m.team t",Member.class)
            .getResultList();
    }

// 2.fetch조인
    public List<Member> findAllWithFetchJoin(){
        return em.createQuery("select m from Member m join fetch m.team ",Member.class)
            .getResultList();
    }

 

일반 조인 테스트 코드

@Test
    public void 일반조인() throws Exception{
        List<Member> members  = memberRepository.findAll(); // 1
        for (Member member:members) {
            System.out.println("member.class = " + member.getTeam().getClass());
            System.out.println("member.class init= " + Hibernate.isInitialized(member.getTeam()));
            System.out.println("member name = " + member.getTeam().getName());
            member.getTeam().getName();
        }
    }

 

혹시 쿼리가 몇 번 나올 것인지 예상되시나요? 

처음 모든 Member를 가져오는 쿼리 한번 , 해당 멤버의 팀을 가져오는 쿼리 1번(멤버의 팀이 달랐다면 2번 쿼리가 나갔을 텐데 첫 번째 유저의 팀을 조회했을 때 영속성 컨텍스트에 저장되어 있기 때문에 1차 캐시에서 가져오고 DB조회 안 함)

도합 2번 나갑니다.

 

조회된 일반 조인 SQL

    select
        member0_.member_id as member_i1_4_,
        member0_.city as city2_4_,
        member0_.street as street3_4_,
        member0_.zipcode as zipcode4_4_,
        member0_.name as name5_4_,
        member0_.team_team_id as team_tea6_4_ 
    from
        member member0_ 
    inner join
        team team1_ 
            on member0_.team_team_id=team1_.team_id
member.class = class jpabook.jpashop.domain.Team$HibernateProxy$cRvKZ9yd -- 프록시객체
member.class init= false
2025-05-12 15:02:34.096 DEBUG 2092 --- [    Test worker] org.hibernate.SQL                        : 
    select
        team0_.team_id as team_id1_7_0_,
        team0_.name as name2_7_0_ 
    from
        team team0_ 
    where
        team0_.team_id=?
member name = Team1
member.class = class jpabook.jpashop.domain.Team$HibernateProxy$cRvKZ9yd-- 프록시객체
member.class init= true
member name = Team1

 

로그를 보면 member에서 team 객체에서 프록시 객체를 넣어 지연로딩 합니다.

그리고 나는 프록시 객체에서 초기화하면 실제 객체로 바뀌지 않을까? 했는데 그냥 초기화 해주고 로그를 보면 프록시 객체 그대로 쓰는 것 같다.

 

그러면 fetch 조인은 어떻게 될까요?

 

Fetch조인 테스트 코드

    @Test
    public void 페치조인() throws Exception{
        List<Member> membersFetch  = memberRepository.findAllWithFetchJoin(); // 1

        for (Member member:membersFetch) {
            System.out.println("membersFetch = " + member.getTeam().getName());
            member.getTeam().getName();
        }
    }

또 예상해 보시죠 ㅋ

쿼리가 몇 번 나갈까요?

한번 나갑니다.

fetch조인은 연관된 엔티티도 다 가져와 영속성 컨텍스트에 저장하기 때문에 연관객체를 프록시 객체로 가져오지 않고 실제 객체로 가져옵니다.

그래서 밑에서 team을 조회해도 다시 db에서 조회하지 않고 영속성 컨텍스트에 있는 값을 가져옵니다.

 

조회된 Fetch 조인 SQL

select
        member0_.member_id as member_i1_4_0_,
        team1_.team_id as team_id1_7_1_,
        member0_.city as city2_4_0_,
        member0_.street as street3_4_0_,
        member0_.zipcode as zipcode4_4_0_,
        member0_.name as name5_4_0_,
        member0_.team_team_id as team_tea6_4_0_,
        team1_.name as name2_7_1_ 
    from
        member member0_ 
    inner join
        team team1_ 
            on member0_.team_team_id=team1_.team_id
member.class = class jpabook.jpashop.domain.Team
membersFetch = Team1
member.class = class jpabook.jpashop.domain.Team
membersFetch = Team1

로그를 보면 실제객체를 참조하고 있는 게 보인다 ㅋ

 

정리해 보면 일반 조인은 값을 가져오긴 하지만 연관된 객체는 프록시 객체로 가져오고

fetch조인은 연관된 객체 모두 실제 객체로 초기화하여 영속성 컨텍스트에 저장한다!

 

 

여기까지만 알아보자 ㅋ

틀린 내용이 있다면 댓글 부탁드립니다 ㅋ

반응형

오 늘  할  일

- Querydsl 강의듣기

- Querydsl로 기능 구현


공 부 기 록

 

queryDsl( jpql vs querydsl ~

jpql vs querydsl

jpql - 문자열이여서 사용자가 해당 메소드를 호출했을 때 런타임에러 발생

querydsl - 컴파일시점에 에러 발생 QType을 만들어내서 자바코드로 품

 

컴파일 시점 오류 발생, 파라미터 바인딩을 자동으로 해줌 

 

Q-type

Q클래스 인스턴스 사용하는 2가지 방법

별칭 직접 지정 : QReview qReview = new QReview("r"); -> 같은 테이블을 조인해야하는 경우에 사용

기본 인스턴스 사용 : QReview aReview = QReview.review;

 

결과 조회

fetch - 리스트 조회 , 데이터 없으면 빈 리스트 변환

fetchOne - 단건 조회 - 없으면 null, 둘 이상이면 NonUniqueResultException

fetchFirst - limit(1).fetchOne()

fetchResults - 페이징 정보 포함, total count 쿼리 추가 실행 -> 성능이 중요한 쿼리에서는 사용하면 안됨 최적화한 count 쿼리를 따로 날려 줘야함

fetchCount - count 쿼리로 변경해서 count 수 조회

 

정렬

 


Toongather

오늘의 궁금증

1. dto로 변환 할 때는 service 단에서 하는게 나을 까? repository 단에서 바로 dto 로 변환하는게 나을까?


 

회  고

- Querydsl 강의듣기(완료)

- Querydsl로 기능 구현(x)

 

이번 툰게더 사이드 프로젝트 하면서 순수JPA , Spring JPA, Querydsl 다 사용해보려고 했는데

하면서 욕심이라는 생각이 들어서 ㅋ 이번에는 QueryDsl은 제외한 나머지에 집중해보자는 생각이 들었다 ㅋ

화이팅~

 

 

 

반응형

오 늘  할  일

- JPA 강의 듣기

- 툰게더 조건 별 리뷰 조회 테스트 코드 작성


Toongather

아우 오늘 조건별 리뷰 조회 테스트 코드 작성하는데

별점 높은순, 별점 낮은순은 잘 햇는데 최신순에서  막혔다..

BaseEntity 공통 Entity 사용해서 등록날짜, 수정날짜 넣고 있어서 테스트 코드 작성할 때 리뷰를 작성해서 넣으면 등록날짜가 똑같이 들어가서..

테스트 할수가 없다... 아우 결국 해결 못하고 오늘 마무리 그렇다고 테스트를 위해 굳이 등록날짜 변경하는 기능을 만드는건 좀 그렇고.. 

 

내일 다시 해봐야겠다.


회  고

- JPA 강의 듣기 (x)

- 툰게더 조건 별 리뷰 조회 테스트 코드 작성(세미완료)

 

오늘 업무 바빴는데 그래도 오후에 했다...! 내일은 오늘 못한 테스트 코드 마저 작성해보고 jpql로 작성한 쿼리 Spring JPA로도 작성해봐야겠다.

아자아자~

반응형

오 늘  할  일

- 깃 강의

마무리

- 툰게더 컨벤션 정하기


공  부  기  록

init - 

add - git이 관리할 대상의 파일 등록하기

commit - 버전 만들기

pull -원격저장소 받는 것 

status - 파일 상태 확인:  Untracked(관리대상이 아님) git add 하지 않은 상태   Tracked(관리대상임) : git이 관리하는 파일임을 의미    unmodified : 최근 커밋과 비교했을 때 바뀐 내용이 없는 상태    modified : 최근 커밋과 비교했을 때 바뀐 내용이 있는 상태    staged: 파일이 수정되고 나서 스테이지 공간에 올아와 있는 상태 git add 후 상태

 

git ignore 파일clone 원격 저장소의 코드를 컴퓨터에 받아올 수 잇습니다.

 

branch

 

checkout 

  - 브랜치 이동

   - checkout기능이 많아져서 switch , restore가 도입   

   - switch : 브랜치 이동   

   - restore : 파일 수정 내용 복원과 add를 통해 스케이지에 올린 파일 빼낼 때 사용합니다.

 

브랜치 삭제

브랜치 복구


Toongather

 

오늘 본격적으로 프로젝트 시작 전에 필요한 것들을 정했다.

 

1. 깃 마일스톤, 프로젝트 생성
2. 이슈 규칙 / 템플릿
3. PR 규칙 / 템플릿
4. Branch 생성 규칙 
5. commit 메세지 규칙

 

서로의 의견을 취합해서 결론은

프로젝트 생성, 마일스톤은 매주
이슈제목은 [도메인] + 제목 내용은 만들고 
pr은 작은리뷰로 그리고 이슈 닫을 때 close 문구 쓴다
브랜치 - [상태]/이슈번호 로하구
커밋 - [상태] 제목 , 상세내용 추가할거면 엔터치고 줄 글 로쓰기

 

여기서 커밋 메세지 규칙은 참고1,참고2

위에 사이트를 참고해서 쓰기로함

이슈,pr 템플릿을 생성 시 에디터에 자동으로 양식이 등록 되도록 설정했다.

 

내일부터 본격적으로 기능구현이다~~~ 아자아자~!~!~!~!~!~!


 

회  고

오늘 ㅋ 툰게더 시작 전에 결정할 것들이 많아서 결정 다하고 ㅋ 내일부터 기능 구현이다. 깃 전략, pr, 이슈 템플릿 규칙 등등을 정하니깐

본격적으로 시작하는 것 같아서 ㅋ "두근두근" 거린다 ㅋ 그리고 깃에 좀 익숙해진 느낌 ㅋ 일단 고 하면서 깃 강의는 주말에 마무리해야겠다.

그래도 전반적인 느낌은 알 것 같다.

 

내일은 이슈 만들고 마일스톤 만들고 해서 시작해야겠다 ㅋ 그리고 JPA 실무 강의도 좀 봐야겠다.

 

김영한씌가,, 야생형 개발자가 잘 성장한다고 했는데 왜 그런지 알 것 같은 느낌이다.나는 좀 학자형 타입인데 야생형으로 하니깐 모르는게 와다다다 나와서 하나하나 쳐내니깐 성장한 느낌 ㅋ

 

여튼 오늘도 성장했다~!내일도 화이팅~!


 

내 일  할  일

- JPA 실무 강의 듣기

- 툰게더 이슈, 마일스톤 생성

- 기능구현

 

 

오늘 TIL 끝!

반응형

+ Recent posts