1. Spring Core

→ 스프링 프레임워크의 기본적인 기능 담당

→ IoC 컨테이너와 DI 컨테이너를 제공하여 객체지향적인 설계를 촉진하고, 애플리케이션의 유연성과 확장성을 향상시킴

위 그림과 같이 애너테이션을 붙이는 방법만으로 객체 A에서 필요한 객체를 직접 생성하는 방식이 아닌 스프링 컨테이너에서 받아오는 방식으로 코드를 작성할 수 있게 된다.

 

1) IoC (Inversion of Control) 컨테이너

- 객체 생성과 관리 담당

- 빈(Bean) 객체 생성, 의존성 주입(DI) 수행

- 빈의 생명주기 관리

- XML, 애너테이션, 자바 설정파일을 사용하여 빈의 구성 정의

 

2) DI (Dependency Injection) 컨테이너

- 빈 객체간의 의존성 관리

- 런타임 시 빈들의 의존성 주입

- 인터페이스와 구현체 간의 느슨한 결합 가능하게 함

- 생성자 주입, Setter 주입 등 다양한 방법 제공  

 

3) AOP (Aspect Oriented Programming)

- Aspect Oriented Programming을 지원

- 메서드 호출 전/후 등 특정 시점에서 공통적인 로직을 수행할 수 있도록 함

- 애너테이션을 활용하여 간편하게 할 수 있음

 

4) 유틸리티 클래스 및 기능

- 스프링 프레임워크에서 자주 사용되는 유틸리티 클래스 제공

- 프로퍼티 파일을 읽어들이는 등의 기능 지원 

 

 

2. 의존성 주입 (Dependency Injection)

1) 필드 주입 (Field Injection)

→ 의존성 주입을 받을 객체의 필드에 @Autowired 어노테이션을 붙여 의존성 주입을 받는 방법

public class UserServiceImpl implements UserService {

    @Autowired
    private UserRepository userRepository;

    // ...
}

코드 길이가 짧아 간편하지만, 테스트 코드 작성이 어렵고 DI 컨테이너에서 빈을 교체할 수 없어 유연성이 떨어짐

 

2) 수정자 주입 (Setter Injection)

→ 의존성 주입을 받을 객체의 Setter 메서드에 @Autowired 어노테이션을 붙여 의존성 주입을 받는 방법

public class UserServiceImpl implements UserService {

    private UserRepository userRepository;

    @Autowired
    public void setUserRepository(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    // ...
}

선택적인 의존성 주입이 가능하여 유연하게 사용할 수 있지만, Setter 메서드를 public으로 열어두어야 하고, 객체 생성 후 의존성 주입이 완료되어야 하므로 일부 속성이 null일 수 있음

 

3) 생성자 주입 (Constructor Injection)

→ 의존성 주입을 받을 객체의 생성자 파라미터에 @Autowired 어노테이션을 붙여 의존성 주입을 받는 방법

public class UserServiceImpl implements UserService {

    private final UserRepository userRepository;

    @Autowired
    public UserServiceImpl(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    // ...
}

불변성을 보장하고, 생성자를 통해 필수적인 의존성을 주입받기 때문에 객체 생성 시점에 의존성 주입이 완료되어 NullPoiinterException 등의 문제가 발생할 가능성이 적음

 

∴ 생성자 주입을 사용하자!

→ 객체 생성 시점에 필요한 의존성을 완전히 제공하고, 불변성을 유지하는 클래스를 구현할 수 있으며 의존성이 변경되는 경우에도 유연하게 대처할 수 있기 때문에 생성자 주입을 사용하는 것이 안전하다.

 

 

3. Spring Core 모듈 주요 애너테이션

@Component 스프링의 컴포넌트 스캔 기능을 사용하여 빈으로 등록하고자 하는 클래스에 사용
@Autowired 자동 주입을 위한 애너테이션으로 스프링 컨테이너가 해당 타입에 맞는 빈을 자동으로 주입
@Qualifier 여러 개의 빈이 등록되어 있을 때 어떤 빈을 주입할지 선택
@Controller 스프링 MVC에서 컨트롤러 역할을 하는 클래스에 사용
@Service 비즈니스 로직을 처리하는 서비스 클래스에 사용
@Repository 데이터베이스와 관련된 작업을 처리하는 DAO 클래스에 사용
@Configuration 자바 기반의 스프링 설정 클래스에 사용
@Bean @Configuration 클래스에서 스프링 빈을 직접 등록하기 위한 애너테이션
@Scope 빈의 범위를 지정하기 위한 애너테이션
singleton, prototype, request, session, global session 등이 있음
@Value 프로퍼티 값을 주입하기 위한 애너테이션
스프링의 PropertyPlaceholderConfigurer를 통해 값을 가져올 수 있음

 

'네트워크캠퍼스 > SPRING' 카테고리의 다른 글

Request Handling  (0) 2024.02.05
REST API  (0) 2024.02.01
Spring MVC 모듈  (0) 2024.02.01
SOLID 원칙  (0) 2024.01.31
스프링 프레임워크  (0) 2024.01.31

1. SOLID 원칙

1) SRP (Single Reponsibility Principle) : 단일 책임 원칙

한 클래스는 단 하나의 책임을 가져야 하고, 클래스가 변경되어야 하는 이유는 단 하나의 이유여야 한다.

public class User {
    private String username;
    private String password;
    
    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }
    
    public boolean isValid() {
        // 유효성 검사
        return true;
    }
    
    public void save() {
        // 데이터 저장
    }
}

위 클래스는 사용자 정보를 나타내는 클래스이고, 유효성 검사와 데이터 저장은 사용자 정보와 직접적인 연관이 없는 작업이므로 단일 책임 원칙에 어긋난다. → 클래스를 더 작은 단위로 분리하여 단일 책임을 부여하는 것이 바람직하다.

public class User {
    private String username;
    private String password;
    
    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }
}

public class UserValidator {
    public boolean isValid(User user) {
        // 유효성 검사
        return true;
    }
}

public class UserDAO {
    public void save(User user) {
        // 데이터 저장
    }
}

위와 같이 클래스를 분리하여 User 클래스는 사용자 정보만 관리하고, 유효성 검사와 데이터 저장은 각각 다른 클래스에서 담당한다.

→ 코드의 유지보수성이 증가하고, 다른 기능을 추가하거나 변경할 때 영향을 최소화할 수 있다.

 

2) OCP (Open-Closed Principle) : 개방-폐쇄 원칙

→ 기존 코드를 변경하지 않으면서 기능을 확장할 수 있도록 설계해야 한다.

public interface Shape {
    double calculateArea();
}

public class Circle implements Shape {
    private double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    public double calculateArea() {
        return Math.PI * radius * radius;
    }
}

public class Rectangle implements Shape {
    private double width;
    private double height;

    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    public double calculateArea() {
        return width * height;
    }
}

public class AreaCalculator {
    public double calculateArea(Shape[] shapes) {
        double totalArea = 0;
        for (Shape shape : shapes) {
            totalArea += shape.calculateArea();
        }
        return totalArea;
    }
}

shape 인터페이스는 다양한 도형의 면적을 계산하기 위한 공통 기능을 정의하고, Circle과 Rectangle 클래스는 각각 원과 사각형의 면적을 계산하는 구체적인 기능을 구현한다.

AreaCalculator 클래스는 입력받은 여러 도형의 면적을 모두 더해 총 면적을 계산하는 역할을 하고, 이때 shape 인터페이스를 구현한 어떤 도형 클래스도 입력받을 수 있다. 이로 인해 새로운 도형 클래스가 추가되더라도 AreaCalculator 클래스는 수정할 필요 없이 기존의 동작을 그대로 유지할 수 있다.

 

3) LSP (Liskov Substitution Principle) : 리스코프 치환 원칙

→ 서브 타입은 언제나 기반 타입으로 교체할 수 있어야 한다. 자식의 일은 부모의 일보다 작거나 같아야 한다.

    즉, 자식클래스가 부모클래스의 인스턴스 대신 사용될 때 언제나 정상적으로 작동해야 한다.

public class PrintPositiveNum {
    private int num;

    public PrintPositiveNum(int num){
        this.num = num;
    }
    public getNum(){
        if(this.num <= 0){
                throw new RuntimeException("0 이하는 출력 불가능!!!");
        }
        return this.num;
    }
}

public class PrintNum extends PrintPositiveNum {
    @Override
    public getNum(){
        return this.num;
    }
}

public class Client{
    public static void main(String[] args){
        PrintPositiveNum obj = new PrintPositiveNum(1);
        //PrintPositiveNum obj = new PrintNum(-1); // 이 경우 위반
        obj.getNum();
    }
}

위 코드에서 PrintPositiveNum은 부모클래스로 양수만 화면에 출력 가능, 자식클래스인 PrintNum은 모든 범위의 숫자 출력 가능

Client 클래스에서 양수를 주는 경우 어떤 타입이 들어와도 실행가능하지만, 음수나 0의 경우 클라이언트측 코드를 수정해야만 함

부모가 수행가능한 범위 내에서만 오버라이딩을 해야 클라이언트측 코드를 고칠 필요가 없어진다는 것이 리스코프 치환 원칙의 핵심

 

4) ISP (Interface Segregation Principle) : 인터페이스 분리 원칙

→ 인터페이스는 클라이언트에 특화되어야 하고, 클라이언트가 사용하지 않는 메서드는 포함하지 않아야 한다.

public interface Shape {
    double calculateArea();
    double calculateVolume();
}

public class Rectangle implements Shape {
    private double width;
    private double height;

    public double calculateArea() {
        return width * height;
    }

    public double calculateVolume() {
        throw new UnsupportedOperationException();
    }
}

public class Cube implements Shape {
    private double width;
    private double height;
    private double depth;

    public double calculateArea() {
        return 2 * (width * height + width * depth + height * depth);
    }

    public double calculateVolume() {
        return width * height * depth;
    }
}

Shape 인터페이스는 도형의 면적과 부피를 계산하는 두 가지 메서드를 정의하는데, Rectangle 클래스는 면적만 가능하고 부피를 계산할 수 없기 때문에 ISP를 위반한다. 즉, 클라이언트는 Shape 인터페이스를 구현한 모든 클래스에서 부피 계산 메서드를 사용해야 하기 때문에 불필요한 의존성이 발생된다.

public interface Area {
    double calculateArea();
}

public interface Volume {
    double calculateVolume();
}

public class Rectangle implements Area {
    private double width;
    private double height;

    public double calculateArea() {
        return width * height;
    }
}

public class Cube implements Area, Volume {
    private double width;
    private double height;
    private double depth;

    public double calculateArea() {
        return 2 * (width * height + width * depth + height * depth);
    }

    public double calculateVolume() {
        return width * height * depth;
    }
}

위와 같이 클라이언트가 자신이 사용하지 않는 메서드에 의존하지 않도록 인터페이스를 작게 분리해야 한다.

 

5) DIP (Dependency Inversion Principle) : 의존 역전 원칙

→ 상위 수준 모듈은 하위 수준 모듈에 의존하지 않아야 하고, 추상화는 구체적인 사항에 의존하지 않아야 한다.

    ① 고차원 모듈은 저차원 모듈에 의존해서는 안 된다.

    ② 추상화는 세부사항에 의존해서는 안된다.

public class RedLight {
    public void turnOn() {
        System.out.println("Red Light turned on");
    }
}

public class Switch {
    private RedLight light;
    
    public Switch() {
        this.light = new RedLight();
    }
    
    public void flip() {
        if (light != null) {
            light.turnOn();
        }
    }
}

위 코드에서 Switch 클래스는 RedLight 클래스를 직접 생성하고 사용하므로 Switch 클래스가 Redlight 클래스에 의존하게 된다.

만약 RedLight 클래스를 BlueLight 클래스로 변경한다면 Switch 클래스도 변경해야 하는 문제가 발생한다.

public interface Light {
    void turnOn();
}

public class RedLight implements Light {
    @Override
    public void turnOn() {
        System.out.println("Red Light turned on");
    }
}

public class Switch {
    private Light light;
    
    public Switch(Light light) {
        this.light= light;
    }
    
    public void flip() {
        if (light!= null) {
            light.turnOn();
        }
    }
}

위 코드에서 Switch 클래스는 Light 인터페이스를 통해 RedLight 클래스와 의존 관계를 맺는다. 이렇게 함으로써 RedLight 클래스에 변경이 생긴다고 해도 Switch 클래스는 영향을 받지 않는다.

'네트워크캠퍼스 > SPRING' 카테고리의 다른 글

Request Handling  (0) 2024.02.05
REST API  (0) 2024.02.01
Spring MVC 모듈  (0) 2024.02.01
Spring Core 모듈  (0) 2024.01.31
스프링 프레임워크  (0) 2024.01.31

1. 스프링 프레임워크

1) 스프링 프레임워크란?

- 자바 언어를 위한 오픈소스 경량급 애플리케이션 프레임워크

- 도메인 객체를 다루기 위한 포괄적인 프로그래밍 및 테스트를 위한 가벼운 솔루션 제공

- IoC(Inversion of Control)와 AOP(Aspect Oriented Programming)를 적용한 경량 컨테이너로 구성

  → 객체의 생명주기와 의존성 관리 해결 및 프로그래머가 비즈니스 로직에 집중할 수 있도록 도움

- 여러 모듈로 구성되어 있으며 필요한 모듈만 선택하여 사용 가능

- 대표적인 모듈 : 스프링 코어, 스프링 MVC, 스프링 데이터 등

- 다양한 개발 환경과 통합이 가능하다는 장점

- 대규모 애플리케이션에서도 유지보수가 용이하고, 테스트 코드 작성이 쉬움

 

2) 스프링 프레임워크를 구성하는 대표적인 모듈

① Spring Core : 스프링 프레임워크의 핵심 모듈, IoC와 DI 기능 제공

② Spring MVC : 웹 개발 시 사용, MVC 패턴을 기반으로 웹애플리케이션 구성

③ Spring JDBC : JDBC를 사용하기 쉽도록 간단한 인터페이스 제공

④ Spring ORM : ORM 프레임워크를 사용하기 쉽도록 지원

⑤ Spring Security : 보안을 쉽게 구현할 수 있도록 지원 → 인증, 권한, 인가 등의 보안 기능

⑥ Spring Test : 테스트를 쉽게 작성할 수 있도록 지원, JUnit과 연동하여 작성 가능

⑦ Spring Data : 데이터 액세스를 지원하는 모듈

 

☆ 스프링 프레임워크는 강력한 객체지향 프로그램을 만들기 위한 도구이다.

- SOLID 원칙을 엄격하게 준수

- 다형성, 캡슐화, 추상화 등을 활용하여 결합도를 낮추고 유연하고 확장성 있는 코드 작성

- 코드의 가독성과 유지보수성, 확장성, 재사용성 높여 개발 생산성 향상

'네트워크캠퍼스 > SPRING' 카테고리의 다른 글

Request Handling  (0) 2024.02.05
REST API  (0) 2024.02.01
Spring MVC 모듈  (0) 2024.02.01
Spring Core 모듈  (0) 2024.01.31
SOLID 원칙  (0) 2024.01.31

※ LXC로부터 시작된 도커의 컨테이너 가상화 기술은 현재는 도커엔진이라는 자체 스펙으로 바뀌었지만, 근간은 여전히 LXC에 있다.

 

1. 컨테이너 내부 측정

처음 가상머신 생성 시 위와 같이 도커가 깔리는 드라이버와 OS가 깔리는 드라이버로 나누었다.

 

/var라는 폴더에 이미지라는 특정 OS와 특정 프로그램이 패키징된 스냅샷을 찍어두고 프로세스를 띄우는 방식으로 진행한다.

이 때 layer라는 개념을 통해 각각의 구성 요소를 조합하게 되고, 이미지 레이어는 Read Only 레이어와 Read, Write가 가능한 컨테이너 레이어가 있다.

 

이미지를 이용해 띄운 컨테이너는 내부적으로 OS가 먼저 실행되며 PID 1번 부여받음, 추가로 네트워크, CPU, 메모리, 디스크 등도 할당받음

이는 리눅스로 수행할 수도 있지만 편의성 증대를 위해 도커라는 기술을 활용한다.

 

내부 구조를 가져오기 위해 먼저 pull과 run을 이용하여 ubuntu 14.04 버전의 bash를 띄워준다.

ls로 조회해보면 시스템 영역에서 사용할법한 디렉터리들(opt, proc, root, run sbin 등)이 위치하는 것을 확인할 수 있다.

이를 chroot 혹은 pivot_root라고 부른다. → root file system을 구성하게 도와주고, 이를 통해 독립성이 보장된다.

 

컨테이너 안에서 조회해보면 위와 같이 디바이스의 사용량이나 가용 자원 등에 대해 나오는데, 특이점은 host docker가 설치된 영역인 /dev/sdb1이 그대로 공유되는 것을 볼 수 있다.

pivot_root와 mount namespace가 위 사례이다.

mount namespace : 장비를 mount시키는 것처럼 가동하는 기술

 

UTS namespace : 컨테이너 아이디와 호스트네임을 동기화시키는 작업

모든 OS는 호스트네임을 가지고, 컨테이너가 생성되면 container id가 부여된다. 해당 ID가 hostname으로 사용된다.

해당 프로세스가 작업을 수행하는데 필요한 것들을 격리시키는 기술인 PID 혹은 IPC namespace라는 기술이 적용된 것을 볼 수 있다.

ifconfig로 확인해보면 위와 같이 eth0에 내부 IP가 할당된 것을 볼 수 있고, 이를 network namespace라고 부른다.

위와 같은 여러 기술이 어우려져 host와는 격리된 환경에서 컨테이너가 하나의 프로세스로 동작한다.

 

 

2. 컨테이너 격리 기술

chroot 프로세스의 루트 디렉토리를 변경, 격리하여 가상의 루트 디렉토리 배정
pivot root 루트 파일시스템을 변경, 컨테이너가 전용 루트 파일시스템을 가지도록 함
mount namespace namespace 내에 파일 시스템 트리 구성
uts namespace host와 다른 별개의 hostname을 가지도록 함
pid namespace pid와 프로세스 분리
(systemd와 분리, 우분투의 1번 프로세스인데 잡히지 않음)
network namespace 네트워크 리소스 할당 (ip, port, route table, ethernet 등)
ipc namespace 전용 process table 보유

 

위와 같이 격리된 namespace 목록은 위와 같이 lsns 명령어로 조회할 수 있다.

 

 

3. 컨테이너의 라이프사이클

① create : image의 스냅샷으로 /var/lib/docker 영역 내에 컨테이너 생성

② start : process 영역에 컨테이너를 생성하여 실행 상태로 만들어줌

③ stop : process 영역에 컨테이너를 제거하여 종료 상태로 만들어줌

④ rm : create로 생성된 스냅샷 삭제

먼저, pull로 ubuntu:16.04 버전을 받아온다.

create 명령어로 새로운 컨테이너를 생성하고 조회해보면 위와 같이 Created 상태의 컨테이너가 만들어진 것을 확인할 수 있다.

Created 상태는 스냅샷을 만들어두기만 하고 프로세스로 가동은 하지 않은 상태이다.

프로세스로 올려서 실행시키기 위해 start 명령을 이용하고 확인해보면 위와 같이 Up 상태로 바뀐 것을 확인할 수 있다.

stop 명령어를 이용하고 확인해보면 위와 같이 Exited 상태로 바뀐 것을 확인할 수 있다.

Exited 상태는 프로세스로 구동만 안하고 있을 뿐 구동 준비는 된 상태이기 때문에 언제든 다시 start가 가능하다.

rm 명령어로 컨테이너를 삭제해주면 위와 같이 아무것도 조회되지 않는 것을 확인할 수 있다.

create와 start를 합친 run 명령어를 이용하면 위와 같이 바로 bash 창으로 들어가는 것이 가능하다.

# apt update
# apt -y install net-tools

각 컨테이너들은 격리환경이기 때문에 필요한 업데이트나 의존성 설정은 처음부터 해줘야 한다.

ifconfig로 확인하면 위와 같이 잘 출력되는 것을 볼 수 있다.

 

 

4. 컨테이너 내부 구조

먼저, ubuntu14.04 버전의 컨테이너를 띄운 후 위와 같은 작업들을 수행했다.

위와 같이 프로세스를 조회해보면 도커 컨테이너가 프로세스로 잡혀있는 것을 확인할 수 있다.

 

※ 스냅샷(이미지)은 Read only이지만, 컨테이너(프로세스)는 Read, Write가 가능하다.

 

root 권한 상태로 변경 후 find 명령어로 container.txt를 찾아보면 위와 같은 결과를 확인할 수 있다.

merged 경로로 이동한 후 ls를 해보면 컨테이너 내부의 파일 구성이 그대로 보이는 것을 확인할 수 있다.

즉, 컨테이너 내부는 사실 overlay2라는 스토리지 영역에 포함된 컨테이너 아이디 폴더에 종속되어 있는 파일들의 집합이라는 것을 알 수 있다.

다른 터미널에서 조회해보면 위와 같이 같은 내용이 나오는 것을 확인할 수 있다.

루트권한에서 a라는 파일을 생성한 후 다른 터미널에서 컨테이너 내부를 조회하면 잘 동기화되는 것을 확인할 수 있다.

 

☆ 즉, 컨테이너 내부에서 생성된 정보는 container layer + snapshot 영역에 저장된다.

 

 

5. 컨테이너 운영 명령어

먼저, 컨테이너 운영을 살펴보기 위한 노트 웹서버를 구축하기 위해 다음과 같은 파일을 작성해준다.

- app.js

const http = require('http');

const server = http.createServer().listen(5678);

server.on('request', (req, res) => {
    console.log('request');
    res.write("HostName: " + process.env.HOSTNAME + "\n");
    res.end();
});

server.on('connection', (socket) => {
    console.log("connected.");
});

 

- Dockerfile

FROM node:20-alpine3.17            // 어떤 os와 프레임워크 위에서 돌릴지
RUN apk add --no-cache tini curl   // 알파인 리눅스이므로 apk로 구동
WORKDIR /app                       // /app이라는 경로 생성 후 cd /app 실행
COPY app.js .                      // Dockerfile과 같은 경로의 app.js를 현재경로에 복사	
EXPOSE 5678                        // 포트바인딩 시 컨테이너 측의 노출포트가 5678
ENTRYPOINT ["/sbin/tini", "--"]	   // 내부적으로 app.js를 실행해주는 명령어
CMD ["node", "app.js"]             // (ENTRYPOINT와 CMD)

 

위 명령어로 이미지를 생성한다.

위와 같이 history 명령어로 nodeapp의 내용을 확인해볼 수 있다.

run 명령어로 컨테이너를 실행하고, 조회해보면 위와 같이 잘 띄워진 것을 확인할 수 있다.

-h 옵션은 hostname을 nodeapp으로 구성하겠다는 의미이다. 해당 태그를 주지 않으면 컨테이너 아이디가 hostname으로 부여된다.

 

※ 세부옵션

--env 컨테이너의 환경변수 지정
-d
--detach=true
백그라운드 실행모드 활성화, 컨테이너 아이디 등록
-t TTY 할당 (bash창 열어주기)
-i
--interactive
대화식 모드 열기 (컨테이너 내부에 명령어 주고받기)
--name 실행되는 컨테이너에 이름 부여 (미지정 시 랜덤한 2단어 조합명으로 부여)
--rm 컨테이너 종료 시 자동으로 컨테이너 제거 (stop 시 삭제)
--restart 컨테이너 종료 시 적ㅈ용할 재시작 정책 지정
(no, on-failure, on-failure:n(횟수), always)
-v
--volume=호스트경로:컨테이너경로
볼륨설정 (볼륨마운트)
-h 컨테이너의 호스트명 지정 (미지정 시 컨테이너 아이디를 호스트명으로 등록)
-p 호스트포트:컨테이너포트
--publish
호스트 포트와 컨테이너 포트를 바인딩
-P
--publish-all=true|false
컨테이너 내부의 EXPOSE 포트를 랜덤포트와 바인딩
--workdir
-w
컨테이너 내부의 작업 경로 (디렉터리)

 

docker top 명령어를 이용하여 컨테이너에서 현재 실행 중인 프로세스의 상태를 볼 수 있다.

docker port 명령어를 이용하여 포트정보를 알 수 있고, IPv4와 IPv6에 대한 정보가 모두 나온다.

위 명령어를 이용해 docker-proxy라는 대리포트 값을 조회할 수 있다.

위에서 얻은 프록시 값으로 위 명령어 입력 시 해당 포트바인딩 명령어의 정보가 저장된 위치가 나온다.

docker stats 명령어로 실시간으로 어떻게 자원을 소비하고 있는지 확인할 수 있다.

해당 컨테이너로 curl이나 브라우저 접속을 유도하면 갑자기 사용량이 증가하는 것을 관찰할 수 있다.

컨테이너명을 여러개 적으면 동시에 조회도 가능하며 흐름에 따른 갱신을 보고싶지 않다면 --no-stream 옵션을 추가하면 된다.

 

 

6. 모니터링용 이미지 및 컨테이너로 상태 감시

1) cadvisor 

→ docker stats로도 상태를 감시할 수 있지만 좀 더 전문적으로 감시할 수 있는 툴

→ 도커허브가 아닌 gcr에 올라와 있기 때문에 아래와 같은 명령어들로 볼륨마운트를 해야만 볼 수 있다.

$ docker run \
--restart=always \
--volume=/:/rootfs:ro \
--volume=/var/run:/var/run:rw \
--volume=/sys/fs/cgroup:/sys/fs/cgroup:ro \
--volume=/var/lib/docker/:/var/lib/docker:ro \
--volume=/dev/disk/:/dev/disk:ro \
--publish=8765:8080 \
--detach=true \
--name=cadvisor \
--privileged \
--device=/dev/kmsg \
gcr.io/cadvisor/cadvisor:latest

위와 같이 이미지를 받은 후 조회해보면 컨테이너가 잘 띄워진 것을 확인할 수 있다.

해당 IP와 포트로 접속해보면 위와 같이 잘 접속된다.

위와 같이 매트릭 형태로 실시간 트래픽들을 확인할 수 있다.

 

 

리눅스에서 반복문 수행 시 헬스체크를 수행하도록 할 수 있다.

※ 헬스체크 : 서버의 가동 여부를 지속적으로 확인하는 행위

$ while true; do curl 노드서버주소; sleep 초단위; done

위와 같은 반복문 수행 시 sleep으로 딜레이를 주기 때문에 해당 초마다 한 번씩 노드서버에 접속하게 된다.

 

console.log()를 이용하여 nodeapp에 지속적으로 요청을 넣어 찍히는 log를 확인할 수 있다.

 

 

 

 

 

 

 

 

 

 

7. cp 명령어로 컨테이너 내부에 호스트 파일 복사하기

간단한 파일 전송 시에는 볼륨마운트 대신 cp 명령어를 이용하여 넘기기도 한다.

$ docker cp 호스트파일명 컨테이너명:경로와파일명

 

 

 

 

 

 

 

 

 

 

 

 

 

※ 일반적인 기업 내부 프로젝트용 이미지는 OS나 프레임워크 등에 대한 정보가 포함되어 있고 의존성 정보도 노출하기 때문에 프라이빗 저장소를 직접 구축하여 사용하기도 한다. → 도커허브에 공개된 registry라는 공식 이미지를 이용하여 구축하는 경우가 많다.

 

1. registry 구축

먼저, registry를 받아오고 확인해보면 25MB 정도로 용량이 상당히 작은 것을 볼 수 있다.

$ docker run -d \
-v /home/유저명/registry_data:/var/lin/registry \
-p 5000:5000 \
--restart=always \
--name=private-registry \
registry

-v 옵션 : 볼륨 → 호스트의 저장폴더와 컨테이너의 저장폴더를 동기화

위 명령어를 통해 호스트의 /home/jihyun/registry_data 폴더에 있는 파일을 컨테이너의 /var/lin/registry 폴더와 즉시 동기화한다.

포트 5000번으로 포트바인딩을 꺼지지 않고 재시작하도록 하는 registry 컨테이너를 띄운다.

history 명령어로 확인해보면 노출시키는 포트가 5000번이고 VOLUME 역시 이미 컨테이너측의 경로가 지정되어 있다.

위와 같이 컨테이너가 잘 띄워진 것을 확인할 수 있다.

위 명령어로 저장 요소를 검색해보면 현재 컨테이너에는 별다른 저장자료가 없기 때문에 위와 같이 출력된다.

위와 같이 보통 alias로 등록해두고 사용한다.

도커허브에 올릴 때 레포지토리명을 따로 이미지에 반영해줬듯이 위처럼 사설 레포지토리의 내부망 주소를 태그로 붙여준다.

해당 주소로 push 해보면 현재는 도커 엔진이 신뢰할 수 없는 저장소이기 때문에 위와 같은 오류가 발생한다.

위 오류를 해결하기 위해서 현재 컨테이너를 저장소로 등록해주고 반영해야 한다.

/etc/init.d/docker 파일의 DOCKER_OPTS 부분을 위와 같이 수정한 후 저장한다.

또한, 위와 같이 파일 생성 후 insecure-registries라는 key 값에 해당 IP 주소를 리스트 형식으로 등록해준다.

갱신 내역들을 반영하기 위해 도커를 재시작해준 후 docker info 명령어로 확인해보면 위와 같이 잘 등록된 것을 볼 수 있다.

이 상태로 push 해주면 위와 같이 잘 수행되는 것을 확인할 수 있다.

위와 같이 이미지 목록과 해당 이미지의 태그도 잘 조회되는 것을 확인할 수 있다.

 

 

2. 구축한 레지스트리로 이미지를 받거나 업로드하기

더보기

<시나리오>

1) host2에 host1의 주소를 기반으로 해서 DOCKER_OPS 에 환경설정을 하고 갱신

2) host2에서 이미지 생성하기

3) 해당 이미지를 host1 기반 private 레지스트리에 업로드

4) 업로드된 내역을 catalog로 조회하기

5) pull도 받아보기

먼저, 해당 주소가 안전한 저장소임을 등록하기 위해 위와 같이 환경설정을 해주어야 한다.

마찬가지로 insecure-registries라는 key 값에 해당 IP 주소(host1주소)를 리스트 형식으로 등록해준다.

도커를 재시작한 후 조회해보면 위와 같이 해당 IP가 잘 등록되어 있는 것을 확인할 수 있다.

docker pull을 해보면 위와 같이 host1 주소에 있는 이미지를 당겨서 받아오고 있는 것을 확인할 수 있다.

host2에서 hello-world 이미지를 생성했고, 해당 이미지를 host1의 레지스트리에 push 해주었다.

host1에서 catalog와 태그를 조회해보면 위와 같이 잘 추가된 것을 확인할 수 있다.

마찬가지로 pull을 해보면 사설 저장소에 있던 것을 당겨서 받아오는 것을 확인할 수 있다.

1. 도커 이미지

- 도커 이미지는 컨테이너 실행에 필요한 실행 파일, 라이브러리, 설정 값 등을 포함

- 이미지는 새로 만들지 않는 한 원본은 변경되지 않음

- 이미지 생성 및 실행 순서

① 애플리케이션 개발

② Dockerfile에 이미지 생성 프로세스 기입

③ 빌드하여 이미지로 생성

④ 이미지를 활용해 컨테이너 생성

⑤ 컨테이너 작동 여부 테스트

⑥ 도커허브에 업로드

⑦ ①로 돌아가기

 

1) 이미지 다운 방법

$ docker 이미지 pull 옵션 name:태그버전

docker.io : docker hub에서 기본적으로 이미지를 끌어오는 저장소 → 루트폴더처럼 굳이 적지 않아도 되는 주소로 간주

$ docker pull 192.168.31.101:9999/도커이미지

위와 같이 프라이빗 저장소를 지정하는 것도 가능하고, 주로 기업들이 이런 식으로 저장소 정보를 바꿔 사용한다.

이처럼 위와 같이 저장소 주소가 따로 들어가면 docker.io가 아닌 것을 볼 수 있다.

 

2) 이미지 내부 정보

$ docker image inspect 이미지명:버전

위 명령어를 통해 이미지 내부 정보를 JSON 형식으로 확인할 수 있다.

- 주요 정보

① Id : 이미지의 아이디

② RepoTags : 레포지토리:태그명

③ Created : 생성일자

④ GraphDriver : 이미지 레이어 정보

⑤ Architecture : CPU의 아키텍처 정보

 

- httpd:2.4 이미지 내부 정보 확인

.Os : 첫번째 계층에 있는 Os 정보만을 빠르게 찾아준다.

JSON 형식이기 때문에 위와 같이 두 개 이상 타고 들어가야 하는 정보도 있다. 

위 명령어를 통해 Config 아래 노출포트인 ExposedPorts 정보를 얻을 수 있다.

$ docker image history 이미지명:버전

마찬가지로 history를 이용해서도 이미지 정보를 얻을 수 있다.

위와 같이 history 명령어로 주요 정보에 대해 체크할 수 있다.

위와 같은 명령어를 통해 파일 형식으로 저장하는 것도 유용하다.

# cd /var/lib/docker/image/overlay2/distribution/diffid-by-digest/sha256/

이미지 구성을 위한 레이어의 계층별 파일은 distribution ID를 부여받고 위 경로에 저장된다.

# ls 번호일부* 명령을 통해 위 경로에 있는 특정 레이어 파일을 조회할 수 있다.

 

※ 하부에 깔리는 이미지는 불변(Read only)으로 만들어지고, 컨테이너는 이미지 위에 Container Layer가 입출력 가능한 계층으로 추가된다. 

→ 하부 레이어를 수정할 수는 없지만, 추가적인 레이어를 구성할 수는 있다.

 

 

2. 이미지 업로드하기

1) 도커 로그인

- 레지스트리 : Dockerfile을 빌드해서 나온 이미지 혹은 docker commit을 통해 생성한 이미지를 저장하는 사이트 자체

- 레포지토리 : 해당 사이트에 생성한 본인 명의의 저장소

- 기본적으로는 hub.docker.com 저장소를 이용하여 이미지 업로드 → docker push 명령어로 수행

- docker push 

  ① docker login : 깃허브와 깃의 관계처럼 내 레포지토리에 올리기 위한 인증

  ② docker tag : 버전별로 태그라는 것을 붙여서 식별

$ docker login
username : 계정명
password : 비밀번호
$ docker logout // 위 정보 파기

위와 같은 방식의 로그인의 경우 base64로 저장되므로 복호화가 상당히 쉽기 때문에 주의해야 한다.

안전한 인증 처리를 위해 도커허브 사이트 로그인 후 [Account Settings] - [Security] - [Access Tokens]에서 토큰 발급 

$ vi .access_token
$ cat .access_token | docker login --username 본인계정명 --password-stdin

발급받은 토큰으로 .access_token 파일을 생성하여 작성하고, 위 명령어를 사용하여 로그인한다.

위와 같이 로그인이 잘 된 것을 확인할 수 있다.

 

2) 이미지 생성 가상머신과 테스트 가상머신 분리

이미지를 생성하는 가상머신과 테스트 가상머신을 분리하기 위해 완전한 복사로 복제를 진행해준다.

$ sudo hostnamectl set-hostname hostos2

위 명령어를 통해 hostname을 변경해준 후 host가 배정받은 내부망 ip 주소 변경(여기서는 192.168.56.102로 설정) 후 재시작한다.

 

3) 본인 원격 레포지토리에 이미지 업로드

본인 소유의 레포지토리에 본인이 생성한 이미지를 업로드하기 위해서는 다음 조건을 만족해야 한다.

① 이미지가 로컬에 생성되어 있어야 한다.

② 원격 레포지토리가 존재해야 하고, 해당 레포지토리에 접근할 수 있는 아이디로 로그인이 되어있어야 한다.

먼저, 도커허브에서 위와 같이 원격 레포지토리를 생성해준다.

위와 같이 도커허브 레지스트리가 잘 생성된 것을 볼 수 있고, 해당 레지스트리에 어떻게 이미지를 업로드 하는 도커 명령어도 볼 수 있다.

실습용 이미지는 위와 같이 NGINX를 기반으로 index.html을 수정한 myweb 이미지를 1.0과 1.1 버전 2개로 준비한다.

이미지를 확인해보면 레포지토리가 붙은 이미지와 원본이미지의 IMAGE_ID 값은 동일하다는 것을 볼 수 있고, 레포지토리로 분류되는 항목에 저장소 주소(내 계정 이름)도 잘 추가된 것을 확인할 수 있다.

docker push 명령어를 통해 해당 레포지토리에 이미지를 업로드한다.

도커 허브에서 확인해보면 잘 업로드된 것을 볼 수 있다.

 

※ 요구하는 양식을 정확히 맞춘다면 push 명령어로 레포지토리가 자동 생성 되기는 하지만, 일반적으로는 도커허브에 먼저 생성해둔 후 거기에 맞춰 업로드하는 방식을 사용한다. 자동 생성되는 레지스트리는 무조건 public으로 설정된다.

 

1.1 버전도 마찬가지로 push 명령어를 사용하여 업로드해주었고, 이미 존재하는 Layer는 제외하고 변경된 부분만 push되는 것을 확인할 수 있다. 즉, 버전이 바뀌어도 기존에 사용하던 레이어는 불변이므로 특정 지점까지는 같은 레이어를 유지한다.

 

4) 올라간 이미지 다른 호스트에서 검증하기

올라간 이미지가 다른 환경에서도 돌아가는지 검증하기 위해 hostos2에서 확인해봐야 한다.

위와 같이 pull 명령어로 아까 올린 이미지를 받으면 이미지가 잘 조회되는 것을 확인할 수 있다.

또한, run 명령어로 해당 이미지를 이용하여 컨테이너를 띄워보면 잘 돌아가고 있는 것을 확인할 수 있다.

 

5) 도커허브 거치지 않고 이미지 옮겨보기

일반적으로는 도커허브를 매개로 주고받지만, 도커허브를 거치지 않고 전달해야 할 때는 도커의 save 명령어를 통해 로컬파일로 저장한 후 물리 파일을 옮겨놓고 load 명령어로 불러와서 다른 호스트에 장착시킬 수 있다.

 

 

 

6) 이미지 삭제에 유용한 명령어

이미지 전체 삭제
$ docker rmi $(docker images -q) 

특정 이미지명을 포함한 것만 삭제
$ docker rmi $(docker images | grep 이미지명)

특정 이미지명이 포함되지 않은 것만 삭제
$ docker rmi $(docker images | grep -v 이미지명)

상태가 exited인 container 전체 삭제
$ docker rm $(docker ps --filter 'status=existed' -a -q)

 

'네트워크캠퍼스 > DOCKER' 카테고리의 다른 글

CLI에서 컨테이너 관리  (0) 2024.01.31
도커 레지스트리 구축  (0) 2024.01.31
Portainer를 이용해 GUI로 컨테이너 관리  (0) 2024.01.26
로컬 환경에서 도커 다뤄보기  (0) 2024.01.25
Play with Docker  (0) 2024.01.23

<PC에 Nginx 설치 후 포트포워딩하여 내 휴대폰(셀룰러)으로 접속해보기>

먼저, Nginx 설치 후 다운로드한 위치에서 nginx.exe 파일을 실행한 후 localhost에 접속해보면 위와 같이 nginx 페이지에 잘 접속되는 것을 확인할 수 있다.

ipconfig 명령어를 이용하여 해당 공유기의 게이트웨이 주소를 알아낼 수 있다.

게이트웨이 주소로 접속해보면 위와 같이 공유기 관리자 페이지를 확인할 수 있고, 현재 해당 공유기는 브릿지로 연결되어 있고, 게이트웨이를 통해 외부와 통신이 가능하다.

nginx.conf 파일의 내용 중 http 부분에서 server.listen 항목은 nginx 서버가 열리는 포트번호를 의미한다.

해당 포트번호를 3386이라는 임의의 숫자로 설정해주었다. 

또한, index.html 파일을 통해 웹페이지에 출력되는 문구를 위와 같이 수정해주었다.

서버 재시작 후 위와 같이 방금 설정한 포트번호로 localhost에 접속해보면 변경한 대로 잘 출력되는 것을 확인할 수 있다.

앞서 확인했던 와이파이의 IP 주소와 설정한 포트번호로 접속해도 위와 같은 결과가 나오는 것을 볼 수 있다.

먼저, 네트워크 설정 정보를 확인하기 위해 해당 관리자 페이지에 접속을 했다.

위와 같이 IP주소와 게이트웨이주소 등을 확인할 수 있다. 

브릿지는 내부 사설망 간의 연결만 제어가 가능하므로 외부 통신 설정을 하기 위해서는 NAT 장비에서 해줘야 한다.

NAT 장비(Gateway)도 마찬가지로 관리자 비밀번호를 이용하여 접속할 수 있고, 위와 같이 네트워크 설정 탭의 NAT 설정을 통해 포트포워딩을 진행할 수 있다.

내부 IP 주소는 현재 접속된 PC의 IP 주소로 설정했다.

위와 같이 외부포트는 55555로 설정했고, 내부포트는 nginx.conf에서 임의로 설정했던 3386으로 해주었다.

즉, 공유기 IP 주소의 외부포트(55555)로 들어온 요청은 내부 IP 주소의 내부포트(3386)로 포트포워딩하라는 의미이다.

해당 공유기 범위 내에서 nginx 서버가 열린 내 PC의 IP 주소가 211.241.92.180인 것을 확인할 수 있다.

휴대폰 셀룰러를 이용하여 위에서 확인한 IP 주소와 외부포트(55555)로 접속해보면 위와 같이 nginx 서버에 잘 접속되는 것을 확인할 수 있다.

 

<내 핸드폰에서 PC로 어떤 과정을 거쳐서 접속 되는지 그려보기>

 

 

'네트워크캠퍼스 > NETWORK' 카테고리의 다른 글

네트워크 쉽게, 더 쉽게  (0) 2024.02.01
3주차  (0) 2024.02.01
2주차  (0) 2024.01.26
모두의 네트워크  (0) 2024.01.22
1주차  (0) 2024.01.22

+ Recent posts