AWS에서 수많은 컴퓨팅 서비스를 하려면 대규모의 서버 컴퓨터를 모아 둔 곳이 필요할것이다.
이때 한 곳에 전부 다 몰아두면 2가지 문제점이 생긴다.
1. 자연재해가 발생할 경우 모든 서비스 마비
2. 모든 자원이 북미에 있다면, 지구 반대편 아시아 지역은 멀어서 서비스가 느림
이 위의 두가지 문제점을 해결하고자 서비스를 하기 위한 자원들을 여러곳에 분산하여 배치해둠.
현재 어떤 리전에서 서비스를 하는지 나타내는 이미지
가용영역:
가용영역은 리전을 한번 더 분산하여 배치한 것
서브넷팅
서브넷팅은 ip주소 낭비를 방지하기 위해 원본 네트워크를 여러개의 서브넷으로 분리하는 과정.
서브넷팅은 서브넷 마스크의 bit수를 증가시키는 것이라고 생각하면된다.
서브넷 마스크의 비트 수를 1씩 증가시키면 할당할 수 있는 네트워크가 2배수로 증가하고 호스트 수는 2배수로 감소한다.
192.168.32.0/24는 원래 하나의 네트워크였다. 이때 할당 가능한 host의 수는 2의8승-1=254개 이다. 2를 빼는 이유는 첫번째 주소는 네트워크 어드레스로 사용되고 마지막 주소는 브로드캐스트로 쓰이기때문에 할당할 수 없다.
이때 서브넷 마스크의 비트수를 1 증가시켜서(서브넷팅)192.168.32.0/25로 변경하게 되면 네트워크 id 부분을 나타내는 부분이 24비트에서 24비트로 증가하고 호스트id를 나타내는 부분이 8개 비트에서 7개비트로 줄어든다.
즉 할당 가능한 네트워크 수가 2개로 증가하고 각 네트워크당 할당 가능한 호스트수는 2의7승 -2 = 126개로 줄어든다.
예제
P1) 211.100.10.0/24 네트워크를 각 서브넷당 55개의 Host를 할당할 수 있도록 서브넷팅 한다고 하자.
a) 서브넷 마스크를 구하시오.
풀이 : HostID 비트를 생각하면 쉽다. Host ID를 나타내는 비트가 6개라면 2^6-2=62개의 호스트를 할당할 수 있으므로 충분하다. 그렇다면 32개의 비트 중 26개가 서브넷 마스크의 bit개수이므로 1111111.11111111.11111111.11000000 가 서브넷 마스크가 될 것이다. 즉 255.255.255.192이다.
답 : 255.255.255.192
203.230.7.0/24
이 아이피 주소에서 찾아낼수 있는 것은?
네트워크 아이디: 203.230.7.0
할당 가능한 호스트 갯수: 256 - 2개
VPC와 아이피 대역
VPC는 기본적으로 가상의 네트워크 영역이기에 사설 아이피 주소를 가지게됨.
사설 아이피 대역은
10.0.0.0/8
172.16.0.0/12
192.168.0.0/24
이렇게 3개의 대역을 가지며 하나의 VPC에는 네트워크 대역, 혹은 서브넷 대역이 할당 가능함
VPC는 실제로 사용시 VPC자체에서도 서브넷을 나누어서 사용하게됨
예시로 10.0.0.0/16의 아이피 주소를 vpc에 할당한 상황에서, vpc를 원하면 다시 서브넷으로 나눠서(단 vpc를 나눈 서브넷을 다시 나누지는 못함) 각각 서브넷을 원하는 가용영역에 배치하여 사용하게됨
이때 유의할점은 vpc의 서브넷 아이피 대역에서는 실제 네트워크와 달리, 총 5개의 아이피 주소를 호스트에 할당할 수 없다는 것
첫주소: 서브넷의 네트워크 대역
두번째: vpc 라우터에 할당
세번째: 아마존이 제공하는 dns에 할당
네번째: 미래를 위해 예약
다섯번째: 브로드 캐스트 주소
vpc내부적으로 라우터가 있고, 그렇다면 vpc 내부 서브넷끼리 통신이 되겠다
vpc와 외부 통신
기본적으로 사설 아이피 대역은 공용 아이피 대역과 통신이 불가능한데 aws로 인프라를 구축하면 통신이되는 이유는?
운영 체제의 커널이 제공하는 서비스에 대해, 응용 프로그램의 요청에 따라 커널에 접근하기 위한 인터페이스이다.
사용자 프로그램이 디스크 파일을 접근하거나 화면에 결과를 출력하는 등의 작업이 필요한 경우, 즉 사용자 프로그램이 특권 명령의 수행을 필요로 하는 경우, 운영체제에게 특권 명령의 대행을 요청하는 것.
경우에 따라 시스템 콜이 발생했을 때, 추가적인 정보가 필요할 수도 있는데 그러한 정보가 담긴 매개변수들을 OS 에 어떻게 전달할까?
매개변수를 CPU 레지스터에 전달한다. - 전달해야 하는 매개변수 보다 레시스터의 수가 작을 수 있다. 매개변수를 메모리에 저장해 해당 메모리의 주소를 레지스터에 전달 할 수 있다. 매개변수는 프로그램에 의해 스택(stack)에 전달(push) 될 수도 있다. - 2,3 번의 경우 매개변수의 갯수나 길이의 제한이 없기 때문에 선호되는 방식이다.
시스템 콜 예시
cp in.txt out.txt
먼저 사용자로부터 입력을 받는데 이때 I/O 시스템 콜 호출이 필요하다. 이후 'cp' 프로그램을 실행시키면 먼저 'in.txt' 파일이 현재 디렉토리에서 접근 가능한지를 확인하기 위한 시스템 콜이 호출된다. 이때 접근이 불가능하다면 에러를 발생시킨 후 프로그램이 종료되는데 이때 시스템 콜이 호출된다. 파일이 존재해 접근 가능하다면 복사한 파일을 저장하기 위해 'output.txt' 파일명이 있는지 검사하기 위한 시스템 콜이 호출된다. 이 때도 마찬가지로 이 파일 명이 존재하는지 존재하지 않는지 검사하기 위해 시스템 콜을 통해 확인한다. 만약 파일 명이 이미 존재한다면, 덮어 씌워야 할지 아니면, 이어서 붙여야 하는지 User에게 물어볼 수 있는데 만약 저장하고자 하는 파일 이름이 겹치지 않다면, 파일을 저장해야 하는데 이 때도 시스템 콜을 이용한다.
아직 데이터 저장소가 선정되지않아, 우선 인터페이스로 구현 클래스를 변경할 수 있도록 설계
데이터 저장소는 RDB, NoSQl등 다양한 저장소를 고민중인 상황으로 가정
개발을 진행하기 위해서 초기 개발 단계에서는 구현체로 가벼운 메모리 기반의 데이터 저장소 사용
우선 이런식으로 멤버 클래스를 작성해주었다
id와 name의 값을 설정해주고 반환해주는 메서드이고
인터페이스에는 이렇게 회원정보를 저장하고 조회하는 기능을 정의하였다
또한 인터페이스를 구현하는 구체적인 클래스인 MemoryMemberRepository를 아래와 같이 정의해주었다.
public class MemoryMemberRepository implements MemberRepository{
private static Map<Long, Member> store = new HashMap<>();
//store는 회원정보를 저장하는데 사용되는 hashmap이며 회원의 고유 식별자를 키로 사용하고 해당 회원 정보를 값으로 저장
private static long sequence = 0L;
//여기서 이 sequence는 0,1,2 이렇게 키값을 설정해주는것
@Override
public Member save(Member member) {
//멤버 세이브할때 우선 시퀀스값 하나 올려주고
member.setId(++sequence);
store.put(member.getId(), member);
return member;
//이렇게하면 store에 넣기전에 멤버 id값을 세팅해주고
//store에 저장함 그럼 맵에 저장이 되겠지
}
@Override
public Optional<Member> findById(Long id) {
//여기 findbyid는 store에서 추출해야함
//근데 만약 추출되는 값이 null일수도 있으니까 Optional.ofNullable로 감싸줘야함
//이렇게 해주면 클라이언트 측에서 처리 가능
return Optional.ofNullable(store.get(id));
}
@Override
public Optional<Member> findByName(String name) {
//이름을 찾으려면 람다를 써서 그냥 store에서 돌리면됨
//람다를 사용하여서 받은 name이 member안에있는 name이랑 같은지 확인 후 찾으면 반환
//여기서는 findAny에 따로 옵셔널 null값 처리를 안해줘도되는 이유가 이 값이 위에보이는 optional로 처리가 됨
return store.values().stream()
.filter(member -> member.getName().equals(name))
.findAny();
}
@Override
public List<Member> findAll() {
//store에 있는 벨류들이 멤버들인데 이게 반환이 됨
return new ArrayList<>(store.values());
}
}
다음은 구현한 코드를 검증하기 위해 테스트 코드를 작성할 것이다.
( 테스트를 진행할때 자바의 main기능을 통해서 실행하거나, 웹 애플리케이션의 컨트롤러를 통해 해당 기능을 실행할수있다.
하지만 이러한 방법으로 하면 실행시 오랜 시간이 걸리고 반복실행이 어려우며 테스트를 한번에 실행하기 어렵다는 단점이 있다.
따라서 JUint을 통하여 테스트를 실행할것이다. )
package kimtaeyoung.hellospring.repository;
import kimtaeyoung.hellospring.domain.Member;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
class MemoryMemberRepositoryTest {
MemoryMemberRepository repository = new MemoryMemberRepository();
//테스트가 하나씩 끝날때마다 저장소를 다 지움
//why? 테스트 순서는 무작위로 돌아가기에 다시 써야되는 값들이 미리 들어가있거나 하는 에러를 방지하기위해
//테스트가 하나씩 종료될때마다 저장소를 지워주는 메서드를 만들어야함 여기서 aftereach가 콜백함수처럼 매번 실행되는 것
@AfterEach
public void afterEach() {
repository.clearStore();
}
@Test
public void save() {
Member member = new Member();
member.setName("spring");
repository.save(member);
//여기서 값을 꺼낼때 이 findbyid가 옵셔넗이라 뒤에 .get()을 사용하여서 값을 꺼내줌
//근데 이게 좋은 방법은 아님
Member result = repository.findById(member.getId()).get();
//System.out.println("result = " + (result == member));
//이 어설트이퀄에서 첫번째 매개변수는 내가 기대하는것, 두번째가 실제 결과
assertEquals(member, result);
}
@Test
public void findByName() {
Member member1 = new Member();
member1.setName("spring1");
repository.save(member1);
Member member2 = new Member();
member2.setName("spring2");
repository.save(member2);
//이렇게 해주면 spring 1이라는 회원과 spring2라는 회원이 가입이 된 상태
//아까와 같이 옵셔넣은 get으로 꺼내주고 assertequal을 이용하여서 기대하는 값과 실제 결과값 비교하기
// 그래서 member1의 이름이 spring1이니까 여기서 spring1을 찾는 걸해보면 정상 동작됨
Member result = repository.findByName("spring1").get();
assertEquals(member1, result);
}
@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();
assertEquals(2,result.size());
}
}
테스트 코드는 이런식으로 작성해주었다.
테스트 코드는 한번에 실행할경우 순서를 가늠하기 어렵기에 따로 storeclear라는 메서드를 만들어주고 그걸 @AfterEach 어노테이션이 부여된 afterEach 메서드를 사용하여서 매번 테스트 후 저장소를 비워주어서 데이터로 인한 에러를 막아줘야한다.
기능개발후에 테스트 코드를 작성하여 테스트 하는 방법도 있지만 테스트 코드작성 후에 기능개발을 하는 방법도 있다.
즉, 테스트 코드작성은 중요하다
다음은 회원 서비스 클래스를 만들것이다(회원 레포와 도메인을 활용하여 실제 비즈니스 로직을 작성)
멤버 서비스 클래스에는 회원가입(이름중복 회원 판별),전체회원 조회, 단일회원조회 기능을 간단하게 넣었다.
public class MemberService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
//회원가입
public Long join(Member member) {
//메서드로 빼줌
validDuplicateMember(member); // 중복 회원 검증
memberRepository.save(member);
return member.getId();
}
//같은 이름 중복 회원 판별 메서드
private void validDuplicateMember(Member member) {
Optional<Member> result = memberRepository.findByName(member.getName());
//ifpresent는 저 result에 널이 아니라 어떤 값이 있으면 동작하는 것 이게 옵셔넗이기에 가능한것
//멤버를 그냥 m이라고 한것
result.ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
//근데 위에있는 코드처럼 적어도되지만 옵셔널이라는 저게 좀 안예뻐서 밑에있는 코드로 대체 가능
// memberRepository.findByName(member.getName());
// .ifPresent(m -> {
// throw new IllegalStateException("이미 존재하는 회원입니다.")
// });
}
//전체 회원 조회
public List<Member> findMembers() {
return memberRepository.findAll();
}
public Optional<Member> findOne(Long memberId) {
return memberRepository.findById(memberId);
}
}
다음은 아까와 마찬가지로 이 기능들이 잘 동작되는지 테스트 코드를 작성할 것이다.
(꿀팁! 클래스 이름에 커멘드+쉬프트+t를 해주면 바로 테스트 케이스 작성 단축키)
테스트 코드를 작성할때
이런식의 given when then 문법을 사용하면 좋음
given: 무언가가 주어짐
when: 무언가를 실행했을때
then: 이런 결과가 나와야함
(테스트가 길면 이런식으로 배 가슴 다리 식으로 나누면 보기 좋음)
ackage kimtaeyoung.hellospring.service;
import kimtaeyoung.hellospring.domain.Member;
import kimtaeyoung.hellospring.repository.MemberRepository;
import kimtaeyoung.hellospring.repository.MemoryMemberRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class MemberServiceTest {
MemberService memberService = new MemberService();
MemoryMemberRepository memberRepository = new MemoryMemberRepository();
//마찬가지로 매번 테스트마다 메모리 초기회
@BeforeEach
public void beforeEach() {
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository);
}
@AfterEach
public void afterEach() {
memberRepository.clearStore();
}
@Test
void join() {
//given
Member member = new Member();
member.setName("hello");
//when
Long saveId = memberService.join(member);
//then
Member findMember = memberService.findOne(saveId).get();
assertEquals(member.getName(), findMember.getName());
}
//테스트 코드 작성시 예외처리 부분도 중요하기에
//아까 처리한 이름중복이되는지 테스트 코드도 작성
@Test
void sameNameJoin() {
//given
Member member1 = new Member();
member1.setName("spring");
Member member2 = new Member();
member2.setName("spring");
//when
memberService.join(member1);
//밑에 try catch 문법 대신에 이런 함수를 활용해서 써도됨
//() -> memberService.join(member2) 이걸 실행했을때 IllegalStateException.class 이게 터져야한다는뜻
IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
assertEquals("이미 존재하는 회원입니다.", e.getMessage());
// try{
// memberService.join(member2);
// fail();
// } catch (IllegalStateException e) {
// //발생되는 메세지랑 작성해뒀던 메세지랑 출력값을 비교하여 제대로 되는지 비교
// assertEquals("이미 존재하는 회원입니다.", e.getMessage());
// }
//then
}
@Test
void findMembers() {
}
@Test
void findOne() {
}
}
멤버 서비스의 테스트 코드를 작성해주었다.
스프링 빈과 의존 관계 설정
- 회원 컨트롤러가 회원 서비스와 회원 리포지토리를 사용할수있게 의존관계 준비
멤버 컨트롤러가 멤버 서비스를 통해 회원가입을 시켜야하고 멤버 서비스를 통해 회원 조회를 할 수 있어야함(의존 관계가 있다)
controller에노테이션이 있으면 스프링에서 이 에노테이션이있는 것을 가지고있는데 이것을 스프링 빈이 관리되고있다라고함
이렇게 스프링 컨테이너에 hellocontroller가 있는데 컨트롤러 에노테이션이있으면 스프링에 딱떠서 스프링에서 관리를 해줌