전체 글

저번 시간에 도커파일을 만들고, 인스턴스에서 인바운드 규칙 ( 포트포워딩? 이려나) 을 설정을 했습니다.

계속해서 도커를 사용해 배포를 진행해 보겠습니다. 

1. 로컬 환경에서 도커를 설치하고 실행시켜 줍니다.

2. 프로젝트를 도커로 빌드하여 이미지화 시켜줍니다.

 

docker build -t nal-ggu:0.3 --platform linux/amd64 .

도커 빌드를 이용해 이미지화 하는데 태그를 달아줍니다. 

--platform 은 원래 안했었는데, 제 aws 환경에서 amd64 여서 도커를 실행을 못시키는 오류가 있어서 붙였습니다.

 

docker images 를 이용해 방금 제가 올린 이미지가 있는지 확인합니다. 

 

3. 도커 허브를 이용해 이 이미지를 허브에 올립니다.

docker hub에 접속해 새로운 repository 를 생성해줍니다.

"닉네임/프로젝트"  의 형식으로 새롭게 만들 수 있습니다. 

이제 여기에 제 이미지를 올리면 됩니다. 

docker tag nal-ggu:0.3 bbnerino/nal-ggu

이게 그냥은 안올라가더라구요.. image의 태그를 바꿔줍니다. 

docker push bbnerino/nal-ggu

태그를 바꾸게 되면 레포지토리에 푸시를 해줄 수 있습니다. 

 

3. 도커 허브를 이용해 서버에서 이미지를 가져옵니다.

우분투 환경에서도 docker login 을 해줍니다. 

그다음 pull 을 하여 우분투 환경에서 도커 이미지를 가질 수 있게 합니다. 

docker pull nal-ggu:latest

3. 이미지를 도커로 실행시켜 줍니다

docker images 로 확인을 한후 

docker run을 통해 이미지를 실행시킵니다.

 docker run -d -p 3000:3000 bbnerino/nal-ggu:latest

마지막으로 docker ps 를 이용해 어떤 컨테이너가 동작하나 확인해줍니다. 

 

시간이 지나 적다보니 기억으로만 적게되었네요.. 

중간에 필요한 코드나 지식들은 한번 더 만들어 보며 빼먹은 부분을 추가해줄 예정입니다.

저번글에 AWS에 인스턴스를 만들어 봤습니다.
이제 AWS 환경에서 배포를 위해 도커 설치를 해보겠습니다.

 

도커란? 

- 도커는 컨테이너 기반의 가상화 플랫폼입니다.

- 독립된 환경을 만들어서 하드웨어를 효율적으로 활용할 수 있고, 개발 환경 (라이브러리,환경설정) 등이 분리되어 있어서 충돌을 방지할 수 있습니다.

 

도커 설치하기 

sudo apt-get install docker-ce docker-ce-cli containerd.io

설치 후 docker -v 를 이용해 설치가 되었는지 확인합니다.

 

 

환경을 다 세팅했다면 제 프로젝트를 도커를 이용해 올려야 합니다. 

저는 node.js 를 이용해 배포하기 보다 Nginx를 이용했습니다. 

Nginx 란

트래픽이 많은 웹사이트의 확장성을 위해 개발된 경량의 고성능 서버입니다.

적은 자원의 사용으로 높은 성능과 높은 동시성을 목표로 만들었습니다. 

 

Dockerfile 작성하기

FROM node:16.13.2 as build-stage
RUN mkdir /app
WORKDIR /app
EXPOSE 3000
RUN chmod -R 777 /app

COPY package*.json ./
RUN yarn install
COPY . .
RUN yarn build

FROM nginx:stable-alpine as production-stage

RUN rm /etc/nginx/conf.d/default.conf
COPY  ./nginx.conf /etc/nginx/conf.d

COPY --from=build-stage /app/build /usr/share/nginx/html
CMD ["nginx", "-g", "daemon off;"]

- 3000 번 포트를 이용해 배포를 할 것입니다.

nginx.conf

server {
    listen 3000;
    listen [::]:3000;
    server_name 13.209.81.39;
    location / {
      root /usr/share/nginx/html;
      index index.html;
      try_files $uri $uri/ /index.html;
    }
}

서버를 이용하지 않기 때문에 딱히 프록시와 관련된 내용은 없습니다. 

여기서 server_name 은 올릴 주소의 퍼블릭 IPv4 주소입니다. 

인스턴스에서 한가지 더 세팅해야 할 부분이 있습니다. 

보안 파트에서 인바운드 규칙, 아웃바운드 규칙이 있습니다. 

인바운드는 "외부에서 이 ip로 접근할 때 어떠한 포트로 접근하는 것을 허용하겠다" 를 의미하고 

아웃바운드는 "내부에서 외부로 어떠한 규칙을 갖는것만 내보내겠다" 를 의미하는 것 같습니다.

 

그래서 인바운드는 최소한으로 열고,

아웃바운드는 전부 열어두는 방식이 일반적입니다 ( 포트,프로토콜 전체 다 열려있다!) 

저희는 외부에서 3000번으로 접근할 것이기 때문에 인바운드 규칙에 추가를 해줍니다. 

보안그룹 밑의 파란색 id를 누르고  - 인바운드 규칙 편집 - 규칙 추가를 눌러 줍니다. 

유형: TCP , 포트는 3000번  소스 정보는 전체를 의미하는 (0.0.0.0) 를 선택하고 저장해줍니다 

 

도커를 사용하기 위한 준비를 마쳤습니다.

-> 2편으로 

 

'서버' 카테고리의 다른 글

AWS 인스턴스 생성하기  (0) 2022.10.25

AWS 인스턴스 생성하기

2022. 10. 25. 09:38
사이드 프로젝트를 배포를 하고 싶어서 aws를 이용하며 정리한 글입니다. 
- 사이드 프로젝트에는 따로 서버를 두지 않아 서버 배포는 필요없는 상황입니다

 

1. AWS에 로그인을 합니다.

2. EC2 서비스를 이용하여 새로운 환경의 인스턴스를 만들어줍니다. 


이름과 원하는 OS를 선택해줍니다.

1. 우선 익숙한 window 환경을 만들어봤는데 엄청 느리고.. 도커에서 오류가 있어서 사용이 불편했습니다 => pass

2. aws linux  : 아마존에서 만든 리눅스 이려나 하면서 만들어 봤는데 명령어가 다르고, 보안 때문에 사용하기 쉽지 않았습니다.

->이건 다음기회에~

3. ubuntu : 가장 무난한 linux 환경입니다. => 선택

 

 

인스턴스 유형을 골라줍니다 . 

저는 공짜로 이용할 것이기 때문에 우측에 free tier eligible 이 적힌 것을 선택했습니다. 

 

키 페어 를 생성해줍니다.

이 키페어를 이용해서 인스턴스에 로그인이 가능합니다. 

키 페어 생성을 하게 되면 ~.cer 파일로 저장이 되는데 시작할 때 저 파일을 가져와서 로그인을 할 것이기 때문에 잘 간직해야 합니다.

나머지는 다음 기회에 설정하겠습니다. 

이렇게 만들게 되면 나의 인스턴스에 추가가 됩니다. 

3.인스턴스 연결하기 

원하는 인스턴스를 실행 시키고 우클릭을 눌려서 연결을 누릅니다. 

우측 아래에 연결 버튼으로 우분투 환경에서의 터미널을 만날 수 있습니다.

세팅 완료!

'서버' 카테고리의 다른 글

AWS - 프로젝트 배포하기(프론트)  (0) 2022.10.25

Lazy Loading

2022. 9. 22. 21:40
레이지 로딩은 이름부터 알기 쉽습니다.
이미지 로딩을 웹을 띄우고 나서 천천히 하는 것과 사용자의 데이터 낭비를 막아주는 것. 
레이지 로딩에 대해서 알아보겠습니다. 

 

Lazy Loading 이란? 

 

 

레이지 로딩은 이미지에 대한 로딩을 뒤로 미루는 것을 의미합니다. 

필요한 부분만 불러오고 나머지 부분은 사용자가 필요할 때까지 미루는 것으로 사용자의 데이터 낭비를 막아줍니다. 

 

기존의 페이지는 웹 페이지를 열면 브라우저가 모든 이미지를 읽고 불러와서 DOM에 렌더링할 것입니다. 

이미지가 많다면 여는 시간도 오래 걸릴 것이고 데이터도 많이 들 것입니다. 

 

예를 들어 사용자가 검색 엔진에서 한 이미지를 검색하면, 요청한 내용으로 채워진 전체 웹 페이지가 로드됩니다.

이미지가 많기 때문에 전체를 렌더링 하기 위해서는 데이터 양이 많을 것입니다. 하지만 우리가 필요한 이미지는 사실 맨 위의 다섯장 정도 안에 있기 마련입니다. 이런식으로 필요없는 이미지를 불러오게 되면 리소스가 낭비됩니다. 

 

lazy Loading을 사용하면 placeholder 콘텐츠로 작성되며 사용자가 필요할 때만 실제 콘텐츠로 대체됩니다. 

무한 스크롤에서, 아래로 스크롤해서 내리는 애니메이션을 통해  웹 페이지의 내용이 로드되는게 하나의 예입니다. 

 

 

 

Lazy Loading 장점

시간 소비와 메모리 사용량을 줄여 콘텐츠 전달을 최적화할 수 있습니다. 

필요한 웹페이지의 일부가 먼저 로드되기 때문에 시간이 적게 소요되고, 나머지의 로딩이 미뤄져 저장공간이 절약됩니다.

불필요한 코드 실행을 피하고 시간과 공간 자원의 최적화된 사용을 가능하게 합니다. 

 

Lazy Loading 단점

코드가 복잡해질 수 있고, 로드되지 않은 컨텐츠의 부적절한 인덱싱으로 검색 엔진에서 순위에 영향을 미칠수 있습니다.

 

 

 

Lazy Loading 예시

사진이나 영상에 사용하거나 사용자에게 바로 보여지는 페이지에서 이미지를 나중에 불러오는 방법이 있습니다. 

 


Lazy loading 구현을해보겠습니다.

 

방법1 

이미지 로드를 사전에 막고 브라우저에게 해당 이미지를 언제 로드할건지에 대한 것을 알려주어야 합니다. 

이번 방법은 IntersectionObserver API 를 사용해서 쉽게 구현해 보겠습니다.

(실제로 스크롤 이벤트리스터보다 성능도 좋다고 합니다. )

 

Lazy Load 시킨 이미지들을 로딩시키기 위해 해당 이미지들에 옵저버를 적용합니다. 

엘리먼트가 뷰포트에 들어간 것을 확인하기 위해 isIntersecting, intersectionRatio 속성을 사용합니다.

또한 확인이 되면 url을 data-src속성에서 src 속성으로 브라우저가 이미지를 로드하도록 트리거를 일으킵니다. 

그 후 해당 이미지는 lazy 클래스를 제거하고 옵저버를 해제합니다. 

 

const useLazyLoading = () => {
  const imgs = document.querySelectorAll('.lazy');

  const observerCallback = (entries, observer) => {
    entries.forEach(({ isIntersecting, intersectionRatio, target }) => {
      if (isIntersecting && intersectionRatio > 0) {
        target.src = target.dataset.src;
        target.classList.remove("lazy");
        observer.unobserve(target);
      }
    });
  };

  const Intersection = new IntersectionObserver(observerCallback);
  imgs.forEach((img) => Intersection.observe(img));
};

useLazyLoading();
<img class="lazy" data-src="src~" />

 

<img src="src~" />​

 

변경된 모습 (전,후)

 

 

구현을 해서 빨리 스크롤을 내려보면 아직 불러오지 못한 이미지는 배경이 노란색인 것을 확인할 수 있습니다.

 

 

Reference

https://mari-mo.tistory.com/206 , https://onlydev.tistory.com/104

'프론트엔드' 카테고리의 다른 글

프론트엔드 성능 최적화  (1) 2022.09.08
브라우저 동작 원리  (0) 2022.09.08

영역 밖 클릭시 닫기

2022. 9. 19. 11:16
지금까지 모달창을 만들기 위해 Mui나 부트스트랩의 도움을 많이 받았었습니다.
제가 직접 만들어 쓰려고 시도를 했었지만, 딱 하나의 문제가 걸려 계속해서 사용했었습니다. 
바로 영역 밖을 클릭하였을 때, 꺼지게 하는 기능입니다. 

이번 작업에서 셀렉트 박스를 직접 만드는 기회가 생겨 그 기능을 구현해 보았고, 모달창에도 적용시킬 수 있습니다. 

 

방법

셀렉트 박스 전체를 ref 를 이용해 영역을 지정해준 뒤 , 클릭을 하는 곳이 그 영역 안에 있는지 확인을 해주는 방법입니다. 

 

 

1. 사용하고자 하는 모달창을 만들고, state로 관리합니다. 

const [showSelect,setShowSelect] = useState(false)

{showSelect && <SelectBox/>}

 

2. useRef 를 이용해 ref를 만들어줍니다. 

const selectEl = useRef<HTMLDivElement>(null);

<SelcetBox ref={selectEl} />

 

useRef() 로 바로 사용하게 되면 타입지정이 되지않아 undefined로 인식하여 에러가 나기 때문에 타입을 반드시 지정해줍니다.

 

3. 클릭 이벤트를 만들어 클릭 이벤트의 요소가 ref 안에 있는지 확인합니다. 

const handleClickOutside = (e:MouseEvent)=>{
    if(selectEl.current)
      if(!selectEl.current.contains(e.target as Node))
      {
        setShowSelect(false)
      }
}
  
  
useEffect(()=>{
    if(showSelect){
      window.addEventListener('click',handleClickOutside)
    }
    return()=>{
      window.removeEventListener('click',handleClickOutside)
    }
},[showSelect])

useEffect를 이용해 클릭이벤트를 만들어줍니다. 

showSelect가 변할 때마다, 셀렉트창이 열려 있다면 => 클릭할 때마다 함수를 실행시킵니다. 

마우스 이벤트를 확인하고, 그 이벤트의 요소가 셀렉트 ref 요소라면 넘어가고, 요소가 아니라면 끄는 함수입니다. 

 

 

실제로 어떻게 동작하는지 보겠습니다.

우선 selectEl의 current를 확인합니다.

console.log(selectEl.current)

<div  class='select_box'>
  <div class='title'>스펙을 추가해주세요</div>
  <div>
    <span class='cotent'>1</span>
    <span class='cotent'>2</span>
    <span class='cotent'>3</span>
    <span class='cotent'>4</span>
  </div>
</div>

이번에는 셀렉트 박스 안의 아무거나 누르고  클릭 이벤트를 확인합니다. 

console.log(e.target)

<span class='cotent'>3</span>
 
이제 이게 위의 ref에 포함되어있는지 확인을 하면 원하는 기능이 구현됩니다. 
 selectEl.current.contains(e.target as Node) =>  true 

(as Node는 e.target의 타입 오류를 막기 위해 추가해줬습니다.)

 

 

Spring - 서비스 & 테스트

2022. 9. 14. 16:35

서비스에서는 핵심 비즈니스 로직들이  구현됩니다.

마찬가지로 service 의 위치에 class 를 만듭니다.

private final MemberRepository memberRepository;

멤버 리퍼지토리를 들고와 새로운 리퍼지토리를 생성합니다.

 

회원가입

public long join(Member member){
    validateDuplicateMember(member); // 중복 회원 검증
    memberRepository.save(member);
    return member.getId();
}

중복 회원 검증을 하고, 리포지토리에 저장을 합니다.

회원가입을 하고 나서는 Id를 return 하게 됩니다.

private void validateDuplicateMember(Member member) {
    memberRepository.findByName(member.getName())
        .ifPresent(m -> {
            throw new IllegalStateException("이미 존재하는 회원입니다.");
        });
}

위에서 사용한 중복 회원 함수입니다.  리포지토리에서 동일한 이름이 있다면 ?

stateException 을 반환합니다. 

 

public List<Member> findMembers(){
    return memberRepository.findAll();
}
public Optional<Member> findOne(Long memberId){
    return memberRepository.findById(memberId);
}

전체 조회와 개인 조회를 만들어 줍니다.

 

 

서비스 테스트 

테스트를 쉽게 만드는 방법이 있습니다 .

cmd + shift + T 를 이용해 새 테스트를 만들어 주게 되면 자동으로 틀을 잡아줍니다. 

 

하지만 이 서비스는 테스트를 할때마다 새로운 리포지토리에서 진행해야 합니다. 

public MemberService(MemberRepository memberRepository){
    this.memberRepository = memberRepository;
}

service에 새로운 함수를 추가합니다. 서비스만의 리포지토리를 새롭게 정의하겠다는 함수입니다.

 

MemberService memberService ;
MemoryMemberRepository memberRepository;
@BeforeEach
public void beforeEach(){
    memberRepository = new MemoryMemberRepository();
    memberService = new MemberService(memberRepository);
}

@BeforeEach 를 통해 함수 진행 전에 새로운 리포지토리로 초기화를 해줍니다.

 

회원가입

void 회원가입() {
    //given
    Member member = new Member();
    member.setName("spring1");
    // when
    Long saveId = memberService.join(member);
    // then
    Member findMember = memberService.findOne(saveId).get();
    Assertions.assertThat(findMember.getName()).isEqualTo(member.getName());
}

given when then 

3단 문법을 이용해 테스트 케이스를 적으면 형식을 맞출 수 있습니다.

어떤 인스턴스를 사용하며, 어떤 행동을 하게되면, 어떤 결과를 보여주는지 순서로 진행됩니다.

 

 

@Test
public void 중복_회원_예외(){
    // given
    Member member1 = new Member();
    member1.setName("spring1");

    Member member2 = new Member();
    member2.setName("spring1");

    // when
    memberService.join(member1);
    // ()-> 이 행동을 할때 , 앞의 예외가 터져야 한다. => 터지면 통과
    assertThrows(IllegalStateException.class, () -> memberService.join(member2)))
}

중복 확인을 하는 테스트입니다. 똑같은 이름으로 두명이 회원가입을 하려고 할때, 

ExceptionError 가 뜨면 성공인 경우를 구현했습니다.

 

assertThrows ( 결과, 원인 )이 참인지 확인하는 것으로 , ()->행동 이 원인이 되고 stateException이 결과가 됩니다. 

'Spring' 카테고리의 다른 글

Spring - repository 테스트  (0) 2022.09.14
Spring - 도메인 & 리포지토리 만들기  (0) 2022.09.14
Spring - 응답방법  (0) 2022.09.14
첫 Spring  (0) 2022.09.14

Spring - repository 테스트

2022. 9. 14. 14:30
리포지토리를 생성을 해봤으니 테스트를 해봐야 합니다.
테스트는 스프링에게 굉장히 중요한 작업이라고 합니다.
TDD 라고 테스트 주도 개발 이라는 말이 있을 정도로 테스트의 중요성이 큽니다. 

 

테스트를 하기 위해서 똑같은 구조로 만드는 것이 좋습니다. 

MemoryMemberRepositoryTest Class 를 만들고 리포지토리에서 만들었던 함수들을 하나씩 실행해 봅니다.

 

MemoryMemberRepository repository = new MemoryMemberRepository();

repository 라는 새로운 객체를 만들어줍니다. 

 

@Test
public void save(){
    Member member = new Member();
    member.setName("myname");

    repository.save(member);

    Member result = repository.findById(member.getId()).get();
    assertThat(result).isEqualTo(member);
}

@Test 를 이용해서 test를 실행할 수 있습니다.

Assertions.assertThat( A ).isEqualTo(B) 를 이용해 A가 B랑 같은지 확인할 수 있습니다. 

틀리게 되면 오류가 납니다. 

 

나머지 함수들도 확인해줍니다.

@Test
public void findByName(){
    Member member1 = new Member();
    member1.setName("spring1");
    repository.save(member1);

    Member member2 = new Member();
    member2.setName("spring2");
    repository.save(member2);

    Member result = repository.findByName("spring1").get();
    System.out.println(result);
    assertThat(result).isEqualTo(member1);
}
@Test
public void findAll(){
    Member member1 = new Member();
    member1.setName("spring1");
    repository.save(member1);

    Member member2 = new Member();
    member2.setName("spring2");
    repository.save(member2);

    List<Member> result =  repository.findAll();
    assertThat(result.size()).isEqualTo(2);
    System.out.println(result);
}

Test를 한번에 실행하게 되면 순서가 정해져 있지 않기 때문에 오류가 날 것입니다. 

 

그래서 모든 함수들을 순회할 때 마다 실행해주는 방법이 아래와 같습니다. 

@AfterEach
public void afterEach(){
    repository.clearStore();
}

@AfterEach 로 함수들을 실행시킬 때마다 초기화할 수 있습니다.

'Spring' 카테고리의 다른 글

Spring - 서비스 & 테스트  (0) 2022.09.14
Spring - 도메인 & 리포지토리 만들기  (0) 2022.09.14
Spring - 응답방법  (0) 2022.09.14
첫 Spring  (0) 2022.09.14

웹 애플리케이션의 계층 구조를 알아야 프로젝트 구조가 잡힐 수 있습니다.

참조 : https://velog.io/@kureungkureung

 

웹 애플리케이션 계층 구조

 

  • 컨트롤러 : 웹 MVC의 컨트롤러 역할
  • 서비스 : 핵심 비즈니스 로직 구현
  • 리포지토리 : 데이터베이스에 접근, 도메인 객체를 DB에 저장하고 관리
  • 도메인 : 비즈니스 도메인 객체

클래스 의존관계

아직 데이터 저장소가 선정되지 않아서, 인터페이스로만 구현 클래스를 변경할 수 있도록 설계했습니다.

 

서비스 개발에 앞서 아래와 같이 도메인과 리포지토리를 만들어줍니다. 

 

도메인 코드 작성

public class Member { // 모든 계층에서 사용하기 위한 POJO
    private Long id;
    private String name;
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

 

id 와 name을 갖는 객체를 만들어 줍니다. 

또한 id와 name의 getter setter를 자동으로 생성해줍니다.

 

리포지토리 작성

public interface MemberRepository {
    Member save(Member member);
    Optional<Member> findById(Long id);
    Optional<Member> findByName(String name);
    List<Member> findAll();
}

MemberRepository 라는 인터페이스를 만들어줍니다. 

사용할 함수들과 결과를 선언을 해줍니다. 

Optional을 이용해서 감싸주게 되면 null이라도 오류없이 반환하게 됩니다.

 

interface를 만들었으니 함수들을 만들러 가겠습니다.

 

MemoryMemberRepository 를 만들어 줍니다.

public class MemoryMemberRepository implements MemberRepository{
    private static Map<Long,Member> store = new HashMap<>();
    private static long sequence = 0L;

    @Override
    public Member save(Member member) {
        member.setId(++sequence);
        store.put(member.getId(),member);
        return member;
    }

    @Override
    public Optional<Member> findById(Long id) {
        return Optional.ofNullable(store.get(id));
    }

    @Override
    public Optional<Member> findByName(String name) {
        return store.values().stream()
                .filter(member -> member.getName().equals(name))
                .findAny();
    }

    @Override
    public List<Member> findAll() {
        return new ArrayList<>(store.values());
    }

    public void clearStore(){
        store.clear();
    }
}

db 가 없으니 store라는 hashmap을 이용해 진행하겠습니다.

sequence 는 id 값으로 0,1,2 .. 1씩 늘어나서 알아서 저장되게 됩니다. 

 

'Spring' 카테고리의 다른 글

Spring - 서비스 & 테스트  (0) 2022.09.14
Spring - repository 테스트  (0) 2022.09.14
Spring - 응답방법  (0) 2022.09.14
첫 Spring  (0) 2022.09.14

+ Recent posts