<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>challduck</title>
    <link>https://challduck.tistory.com/</link>
    <description>반가워용</description>
    <language>ko</language>
    <pubDate>Thu, 16 Jul 2026 17:39:06 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>challduck</managingEditor>
    <image>
      <title>challduck</title>
      <url>https://tistory1.daumcdn.net/tistory/7081106/attach/275b5a1f6e344402b3f0b6d8e571df20</url>
      <link>https://challduck.tistory.com</link>
    </image>
    <item>
      <title>다형성(Polymorphism) 이란</title>
      <link>https://challduck.tistory.com/35</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 다형성의 기본 개념&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다형성은&amp;nbsp;객체&amp;nbsp;지향&amp;nbsp;프로그래밍의&amp;nbsp;핵심&amp;nbsp;개념&amp;nbsp;중&amp;nbsp;하나로,&amp;nbsp;&quot;많은&amp;nbsp;형태를&amp;nbsp;가진다&quot;는&amp;nbsp;의미를&amp;nbsp;가지고&amp;nbsp;있다. &lt;br /&gt;&lt;br /&gt;다형성을&amp;nbsp;통해&amp;nbsp;하나의&amp;nbsp;Interface나&amp;nbsp;Method가&amp;nbsp;여러&amp;nbsp;다른&amp;nbsp;구현을&amp;nbsp;가질&amp;nbsp;수&amp;nbsp;있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 다형성의 주요 유형&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;a) 컴파일 타임 다형성 (정적 바인딩)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Method 오버로딩을 통해 구현됨&lt;br /&gt;- 동일한 이름의 Method가 다른 매개변수를 가질 수 있다&lt;br /&gt;- 컴파일 시점에 어떤 Method가 호출될지 결정됨&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;b) 런타임 다형성 (동적 바인딩)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Method 오버라이딩을 통해 구현됨&lt;br /&gt;- 부모 클래스의 Method를 자식 클래스에서 재정의&lt;br /&gt;- 실행 시점에 어떤 Method가 호출될지 결정&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 메소드 오버로딩 (Method Overloading)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 같은 Class 내에서 동일한 이름의 Method를 여러 개 정의할 수 있다&lt;br /&gt;- 조건: 매개변수의 개수, 타입, 순서가 달라야 한다&lt;br /&gt;- 반환 타입만 다른 경우는 오버로딩으로 인정되지 않음&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1723800196122&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;   public class Calculator {
       public int add(int a, int b) {
           return a + b;
       }
       
       public double add(double a, double b) {
           return a + b;
       }
       
       public int add(int a, int b, int c) {
           return a + b + c;
       }
   }&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 메소드 오버라이딩 (Method Overriding)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;- 부모 클래스의 메소드를 자식 클래스에서 재정의&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;-&amp;nbsp;메소드&amp;nbsp;시그니처(이름,&amp;nbsp;매개변수,&amp;nbsp;반환&amp;nbsp;타입)가&amp;nbsp;동일해야&amp;nbsp;함&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;-&amp;nbsp;@Override&amp;nbsp;어노테이션을&amp;nbsp;사용하여&amp;nbsp;명시적으로&amp;nbsp;표시할&amp;nbsp;수&amp;nbsp;있다&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1723800661729&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;   class Animal {
       public void makeSound() {
           System.out.println(&quot;The animal makes a sound&quot;);
       }
   }
   
   class Dog extends Animal {
       @Override
       public void makeSound() {
           System.out.println(&quot;The dog barks&quot;);
       }
   }&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;5. Interface를 통한 다형성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;- Interface를 구현하는 여러 Class가 같은 Method를 다르게 구현할 수 있다&lt;/p&gt;
&lt;pre id=&quot;code_1723800702622&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;   interface Shape {
       double getArea();
   }
   
   class Circle implements Shape {
       private double radius;
       
       public Circle(double radius) {
           this.radius = radius;
       }
       
       @Override
       public double getArea() {
           return Math.PI * radius * radius;
       }
   }
   
   class Rectangle implements Shape {
       private double width;
       private double height;
       
       public Rectangle(double width, double height) {
           this.width = width;
           this.height = height;
       }
       
       @Override
       public double getArea() {
           return width * height;
       }
   }&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;6. 추상 클래스를 통한 다형성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;- 추상 클래스는 일부 구현과 추상 메소드를 포함할 수 있다. &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;- 자식 클래스에서 추상 메소드를 구현하여 다형성을 실현한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;* Interface와 Abstract Class의 차이점?&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;추상 클래스(Abstract Class) 특징&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;Abstract 메소드와 구체적인 메소드를 모두 가질 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;모든 종류의 변수(static, non-static, final 등)를 선언할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 생성자를 가질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;Java에서는 다중 상속을 지원하지 않으므로, 하나의 클래스는 하나의 추상 클래스만 상속받을 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;관련된 클래스들 간의 공통적인 특성을 묶어 일반화하는 데 사용되며, &quot;is-a&quot; 관계를 나타낸다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;모든 접근 제어자(public, protected, private)를 사용할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;- &lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;'extends' 키워드를 사용하여 확장한다.&lt;/span&gt; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1723802346749&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;abstract class Animal {
    protected String name;

    public Animal(String name) {
        this.name = name;
    }

    public abstract void makeSound();

    public void eat() {
        System.out.println(name + &quot; is eating.&quot;);
    }
}

class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }

    @Override
    public void makeSound() {
        System.out.println(&quot;Woof!&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;인터페이스(Interface) 특징&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;오직 상수(public static final)만 선언할 수 있다.&lt;/span&gt; &lt;br /&gt;- &lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;생성자를 가질 수 없다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;한 클래스는 여러 인터페이스를 구현할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;클래스의 특정 동작을 보장하는 일종의 계약을 정의하며, &quot;can-do&quot; 관계를 나타낸다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 모든 메소드는 기본적으로 public abstract이며, 변수는 public static final 이다.&lt;br /&gt;- 'implements' 키워드를 사용하여 구현하며 인터페이스는 다른 인터페이스를 'extends'할 수 있다. &lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1723802678805&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface Swimmable {
    void swim();

    default void float() {
        System.out.println(&quot;Floating on water.&quot;);
    }
}

class Fish implements Swimmable {
    @Override
    public void swim() {
        System.out.println(&quot;Fish is swimming.&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;다중 인터페이스 구현 예시&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 다형성의 장점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;- 코드 재사용성 향상&lt;/p&gt;
&lt;pre id=&quot;code_1723803811843&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public void processShape(Shape shape) {
    System.out.println(&quot;Area: &quot; + shape.getArea());
}
// 이 메서드는 Shape 인터페이스를 구현한 모든 클래스(Circle, Rectangle 등)에 대해 사용 가능&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;-&amp;nbsp;유연성&amp;nbsp;증가&lt;/p&gt;
&lt;pre id=&quot;code_1723803821516&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List&amp;lt;Animal&amp;gt; animals = new ArrayList&amp;lt;&amp;gt;();
animals.add(new Dog());
animals.add(new Cat());
animals.add(new Bird());

for (Animal animal : animals) {
    animal.makeSound(); // 각 동물 타입에 맞는 소리를 냄
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;-&amp;nbsp;확장성&amp;nbsp;개선&lt;/p&gt;
&lt;pre id=&quot;code_1723803836528&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface PaymentMethod {
    void processPayment(double amount);
}

class CreditCardPayment implements PaymentMethod { ... }
class PayPalPayment implements PaymentMethod { ... }
// 나중에 새로운 결제 방식을 쉽게 추가할 수 있음
class CryptoCurrencyPayment implements PaymentMethod { ... }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;- 유지보수가 용이함&lt;/p&gt;
&lt;pre id=&quot;code_1723803848876&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface Logger {
    void log(String message);
}

class ConsoleLogger implements Logger { ... }
class FileLogger implements Logger { ... }

class Application {
    private Logger logger;
    
    public Application(Logger logger) {
        this.logger = logger;
    }
    
    public void doSomething() {
        // logger의 구체적인 구현에 상관없이 로깅 가능
        logger.log(&quot;Action performed&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 다형성과 형변환&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;- 업캐스팅: 자식 클래스의 객체를 부모 클래스 타입으로 참조 &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;- 다운캐스팅: 부모 클래스 타입의 참조를 자식 클래스 타입으로 변환 (주의 필요 -&amp;gt; 접은 글 참조)&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다운캐스팅 주의할 점&lt;br /&gt;&lt;br /&gt;a) 런타임 오류 가능성: 실제 객체가 캐스팅하려는 자식 클래스의 인스턴스가 아닐 경우 ClassCastException이 발생&lt;br /&gt;b) 타입 안전성 문제: 컴파일러가 모든 타입 불일치를 감지할 수 없어, 런타임에 예기치 않은 오류가 발생할 수 있음&lt;br /&gt;c) 코드의 안정성 저하: 잘못된 다운캐스팅은 프로그램의 안정성을 해칠 수 있음&lt;/p&gt;
&lt;pre id=&quot;code_1723801004311&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Animal {
    void makeSound() {
        System.out.println(&quot;Some animal sound&quot;);
    }
}

class Dog extends Animal {
    void makeSound() {
        System.out.println(&quot;Woof&quot;);
    }

    void fetch() {
        System.out.println(&quot;Dog is fetching&quot;);
    }
}

class Cat extends Animal {
    void makeSound() {
        System.out.println(&quot;Meow&quot;);
    }

    void scratch() {
        System.out.println(&quot;Cat is scratching&quot;);
    }
}

public class CastingExample {
    public static void main(String[] args) {
        // 업캐스팅
        Animal animal1 = new Dog(); // Dog -&amp;gt; Animal
        Animal animal2 = new Cat(); // Cat -&amp;gt; Animal

        animal1.makeSound(); // 출력: Woof
        animal2.makeSound(); // 출력: Meow

        // 업캐스팅된 객체는 자식 클래스의 특정 메소드를 호출할 수 없음
        // animal1.fetch(); // 컴파일 오류

        // 다운캐스팅
        if (animal1 instanceof Dog) {
            Dog dog = (Dog) animal1; // Animal -&amp;gt; Dog
            dog.fetch(); // 출력: Dog is fetching
        }

        // 안전하지 않은 다운캐스팅 (런타임 오류 발생)
        try {
            Cat cat = (Cat) animal1; // ClassCastException 발생
            cat.scratch();
        } catch (ClassCastException e) {
            System.out.println(&quot;Cannot cast Dog to Cat&quot;);
        }

        // instanceof를 사용한 안전한 다운캐스팅
        if (animal2 instanceof Cat) {
            Cat cat = (Cat) animal2;
            cat.scratch(); // 출력: Cat is scratching
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1723801614652&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Cat cat = (Cat) animal1;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;에서 animal1은 실제로 Dog 객체이므로 Cat으로 캐스팅할 수 없다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;이러한 잘못된 다운캐스팅은 ClassCastException을 발생시키므로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;instanceof 검사나 예외 처리를 통해 다운캐스팅을 수행해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결론&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;- 업캐스팅&lt;/p&gt;
&lt;pre id=&quot;code_1723801654107&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Animal animal1 = new Dog();
Animal animal2 = new Cat();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;자식 클래스의 객체를 부모 클래스 타입의 참조변수에 할당하고 묵시적으로 수행된다.&lt;br /&gt;업캐스팅된 객체는 부모 클래스의 메소드만 사용할 수 있다. (오버라이드된 메소드는 자식 클래스의 구현이 호출됨)&lt;br /&gt;&lt;br /&gt;- 다운캐스팅&lt;/p&gt;
&lt;pre id=&quot;code_1723801709583&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Dog dog = (Dog) animal1; // 다운 캐스팅 가능

Cat cat = (Cat) animal1; // ClassCastException 발생

if (animal2 instanceof Cat) {
    Cat cat = (Cat) animal2;
    cat.scratch(); // 출력: Cat is scratching
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;부모 클래스 타입의 참조를 자식 클래스 타입으로 변환하고 명시적으로 캐스팅 연산자 ()를 사용해야 한다.&lt;br /&gt;instanceof 연산자를 사용해서 안전한 다운캐스팅을 수행할 수 있다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 다형성의 제한사항&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; - private, static, final 메소드는 오버라이딩할 수 없다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;pre id=&quot;code_1723803331207&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Parent {
    private void displayPrivate() {
        System.out.println(&quot;This is a private method in Parent.&quot;);
    }

    static void displayStatic() {
        System.out.println(&quot;This is a static method in Parent.&quot;);
    }

    final void displayFinal() {
        System.out.println(&quot;This is a final method in Parent.&quot;);
    }
}

class Child extends Parent {

    // 아래 메소드는 오버라이딩할 수 없다.
    @Override
    void displayPrivate() { // 컴파일 오류: displayPrivate()는 private 메소드
        System.out.println(&quot;This is a private method in Child.&quot;);
    }

    @Override
    static void displayStatic() { // 컴파일 오류: displayStatic()은 static 메소드
        System.out.println(&quot;This is a static method in Child.&quot;);
    }

    @Override
    void displayFinal() { // 컴파일 오류: displayFinal()은 final 메소드
        System.out.println(&quot;This is a final method in Child.&quot;);
    }
}

public class Main {
    public static void main(String[] args) {
        Child child = new Child();
        child.displayPrivate(); // private 메소드는 접근할 수 없다.
        child.displayFinal(); // final 메소드는 오버라이딩할 수 없다.
        
        Child.displayStatic(); // 정적 메소드는 클래스 이름으로 호출한다.
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- 생성자는 상속되지 않으므로 다형성을 적용할 수 없다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;pre id=&quot;code_1723803521801&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Parent {
    Parent() {
        System.out.println(&quot;Parent constructor called.&quot;);
    }
}

class Child extends Parent {
    // Parent 클래스의 생성자는 상속되지 않음.
    // Child 클래스에서 Parent의 생성자를 명시적으로 호출해야 함.
    Child() {
        // super(); // 부모 클래스의 기본 생성자를 호출
        System.out.println(&quot;Child constructor called.&quot;);
    }
}

public class Main {
    public static void main(String[] args) {
        Child child = new Child(); // Parent 생성자 호출 후 Child 생성자 호출
        
        // 출력 :
        // Parent constructor called.
	// Child constructor called.
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>JAVA</category>
      <author>challduck</author>
      <guid isPermaLink="true">https://challduck.tistory.com/35</guid>
      <comments>https://challduck.tistory.com/35#entry35comment</comments>
      <pubDate>Fri, 16 Aug 2024 19:24:48 +0900</pubDate>
    </item>
    <item>
      <title>Class, Instance, Object란?</title>
      <link>https://challduck.tistory.com/34</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1152&quot; data-origin-height=&quot;698&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3DgJL/btsI5PhfnHL/4MKdM6mSHP237kIu8ERrd0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3DgJL/btsI5PhfnHL/4MKdM6mSHP237kIu8ERrd0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3DgJL/btsI5PhfnHL/4MKdM6mSHP237kIu8ERrd0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3DgJL%2FbtsI5PhfnHL%2F4MKdM6mSHP237kIu8ERrd0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1152&quot; height=&quot;698&quot; data-origin-width=&quot;1152&quot; data-origin-height=&quot;698&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;요약&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Class(클래스)&lt;/b&gt;: Object를 만들기 위한 설계도. 객체의 상태(Field)와 행동(Method)를 정의한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Instance(인스턴스)&lt;/b&gt;: Class의 설계도를 바탕으로 생성된 실제 객체.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Object(객체)&lt;/b&gt;: Class에 의해 정의된 Instance, 상태와 행동을 가지며 특정 클래스의 Instance이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Class (클래스)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Class는 Object를 생성하기 위한 &lt;b&gt;설계도&lt;/b&gt;에 빗대어 설명할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Class는 객체의 상태(Field)와 행동(Method)을 정의한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Class는 특정 타입의 Object가 가져야 할 Attribute와 Method를 정의하는 틀이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자동차를 모델링을 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;예를 들면&lt;/span&gt;&amp;nbsp;자동차 클래스에는 차의 속도, 색상, 제조사와 같은 Attribute(Field)와 가속, 감속, 정지와 같은 행동(Method)이 포함될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;* Java&amp;nbsp;실행&amp;nbsp;시,&amp;nbsp;Class는&amp;nbsp;JVM의&amp;nbsp;메모리에서&amp;nbsp;Class&amp;nbsp;Area(Method&amp;nbsp;Area)에&amp;nbsp;로드된다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1723798964771&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Car {
    // 속성 (필드)
    String color;
    String manufacturer;
    int speed;

    // 생성자
    public Car(String color, String manufacturer) {
        this.color = color;
        this.manufacturer = manufacturer;
        this.speed = 0;
        // 초기 속도는 0으로 설정
    }

    // 행동 (메서드)
    void accelerate(int increment) {
        speed += increment;
    }

    void brake(int decrement) {
        speed -= decrement;
    }

    void stop() {
        speed = 0;
    }

    void displayStatus() {
        System.out.println(&quot;Color: &quot; + color + &quot;, Manufacturer: &quot; + manufacturer + &quot;, Speed: &quot; + speed);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Instance (인스턴스)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인스턴스는 클래스에 정의된 설계를 바탕으로 실제로 생성된 객체를 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스는 그 자체로는 실체가 없지만, 클래스를 기반으로 메모리에 할당된 객체가 생성될 때 이를 인스턴스라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 말해, 클래스는 추상적인 개념이고, 인스턴스는 구체적인 실체이다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1723799131602&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Main {
    public static void main(String[] args) { 
        // Car 클래스의 인스턴스 생성 
        Car myCar = new Car(&quot;Red&quot;, &quot;Toyota&quot;); 
        // myCar는 Car 클래스의 인스턴스 
        myCar.accelerate(50); 
        myCar.displayStatus(); 
        // 출력: Color: Red, Manufacturer: Toyota, Speed: 50 
    } 
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Object (객체)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Object는 클래스에 의해 정의된 인스턴스다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 Instance와 Object라는 용어는 거의 동일하게 사용되지만, Object가 더 일반적인 용어다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Object는 특정 Class의 인스턴스일 수 있다. 객체는 상태(필드)와 행동(메서드)을 가지고 있으며, 이 두 가지가 결합되어 객체의 고유한 특성을 나타내게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1723799363756&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Car myCar = new Car(&quot;Red&quot;, &quot;Toyota&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;* Class를 사용하여 JVM Heap Area에 새로운 Instance(Object)를 생성할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 참조&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Java_virtual_machine&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://en.wikipedia.org/wiki/Java_virtual_machine&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@dongvelop/Java-%ED%81%B4%EB%9E%98%EC%8A%A4-%EA%B0%9D%EC%B2%B4-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4%EC%9D%98-%EC%B0%A8%EC%9D%B4&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@dongvelop/Java-%ED%81%B4%EB%9E%98%EC%8A%A4-%EA%B0%9D%EC%B2%B4-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4%EC%9D%98-%EC%B0%A8%EC%9D%B4&lt;/a&gt;&lt;/p&gt;</description>
      <category>JAVA</category>
      <author>challduck</author>
      <guid isPermaLink="true">https://challduck.tistory.com/34</guid>
      <comments>https://challduck.tistory.com/34#entry34comment</comments>
      <pubDate>Fri, 16 Aug 2024 18:20:43 +0900</pubDate>
    </item>
    <item>
      <title>DataBase란?</title>
      <link>https://challduck.tistory.com/32</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Database는 무엇인가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 진행하면서 필수적으로 사용해야 하는 Database에 대해 알아보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 정의는 아래와 같다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;데이터베이스(database, DB)는 여러 사람이 공유하여 사용할 목적으로 체계화해 통합, 관리하는 데이터의 집합이다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 베이스는 우리의 일상생활과 밀접 연관이 있다. 예시로 SNS 활용, 대중교통 기록, 은행업무 등이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시와 같이 데이터베이스는 현대 사회의 거의 모든 분야에서 필수적인 요소로 자리 잡고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스를 사용하기 전 특징과 다양한 기본지식이 필요하므로 기본개념과 유형에 대해 알아보도록 하자.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;DB의 기본 개념&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스는 단순히 데이터를 저장하는 것 이상의 중요한 목적을 가지고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주된 목적은 데이터의 무결성을 유지하고, 중복을 최소화하며, 빠른 검색과 업데이트를 가능하게 하는 것이다. &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB의 유형은 RDBMS, NoSQL로 나누어지며, 둘의 공통적인 개념은 아래와 같다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;데이터 저장 및 검색&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;데이터 모델:&lt;/b&gt; 두 시스템 모두 데이터를 구조화된 형태로 저장하게 되는데, RDBMS는 Table을, NoSQL은 다양한 형태(Docs, Key-Value 등)를 사용&lt;br /&gt;&lt;b&gt;쿼리 기능:&lt;/b&gt; 데이터를 검색하고 조작하기 위한 Query나 API를 제공한다. RDBMS는 SQL을, NoSQL은 각 데이터베이스에 특화된 쿼리 방법을 사용하게 된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;CRUD 연산&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Create(생성), Read(읽기), Update(갱신), Delete(삭제)라는 기본적인 데이터 조작 연산을 지원&lt;br /&gt;&lt;b&gt;- 생성:&lt;/b&gt; 새로운 데이터 레코드를 추가&lt;br /&gt;&lt;b&gt;- 읽기:&lt;/b&gt; 저장된 데이터를 조회&lt;br /&gt;&lt;b&gt;- 갱신:&lt;/b&gt; 기존 데이터를 수정&lt;br /&gt;&lt;b&gt;- 삭제:&lt;/b&gt; 더 이상 필요하지 않은 데이터를 제거&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Index(색인)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 시스템 모두 데이터 검색 속도를 향상시키기 위해 인덱스를 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;목적 :&lt;/b&gt;&amp;nbsp;빠른 데이터 접근과 검색 성능 개선&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;구현 :&lt;/b&gt; 특정 필드나 속성에 대해 별도의 데이터 구조를 생성하여 관리&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Transaction&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터의 일관성과 무결성을 보장하기 위해 트랜잭션을 지원&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ACID&amp;nbsp;속성:&lt;/b&gt;&amp;nbsp;원자성(Atomicity),&amp;nbsp;일관성(Consistency),&amp;nbsp;고립성(Isolation),&amp;nbsp;지속성(Durability) &lt;br /&gt;&lt;b&gt;구현 수준:&lt;/b&gt; RDBMS가 더 엄격한 ACID 준수를 제공하는 반면, NoSQL은 상황에 따라 유연한 접근을 취한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;확장성 &lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 증가에 대응하기 위한 확장 메커니즘을 제공&lt;br /&gt;&lt;br /&gt;&lt;b&gt;수직적&amp;nbsp;확장(Scale-up):&amp;nbsp;&lt;/b&gt;하드웨어&amp;nbsp;성능&amp;nbsp;향상 &lt;br /&gt;&lt;b&gt;수평적 확장(Scale-out):&lt;/b&gt; 여러 서버에 데이터 분산&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;보안&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 보호를 위한 보안 메커니즘을 제공&lt;br /&gt;&lt;br /&gt;&lt;b&gt;인증:&amp;nbsp;&lt;/b&gt;사용자&amp;nbsp;식별&amp;nbsp;및&amp;nbsp;접근&amp;nbsp;제어 &lt;br /&gt;&lt;b&gt;권한&amp;nbsp;관리:&lt;/b&gt;&amp;nbsp;사용자별&amp;nbsp;데이터&amp;nbsp;접근&amp;nbsp;권한&amp;nbsp;설정 &lt;br /&gt;&lt;b&gt;암호화:&lt;/b&gt;&amp;nbsp;중요&amp;nbsp;데이터의&amp;nbsp;암호화&amp;nbsp;저장&amp;nbsp;및&amp;nbsp;전송 &lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;백업&amp;nbsp;및&amp;nbsp;복구&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 손실 방지와 복구를 위한 기능을 제공&lt;br /&gt;&lt;br /&gt;&lt;b&gt;정기적인&amp;nbsp;백업:&amp;nbsp;&lt;/b&gt;데이터의&amp;nbsp;주기적인&amp;nbsp;복사본&amp;nbsp;생성 &lt;br /&gt;&lt;b&gt;복구 메커니즘:&lt;/b&gt; 장애 발생 시 데이터 복원 방법&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;동시성 제어 &lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 사용자가 동시에 데이터에 접근할 때 발생할 수 있는 문제를 관리&lt;br /&gt;&lt;br /&gt;&lt;b&gt;락(Lock):&lt;/b&gt;&amp;nbsp;데이터&amp;nbsp;접근을&amp;nbsp;제어하는&amp;nbsp;메커니즘 &lt;br /&gt;&lt;b&gt;버전 관리:&lt;/b&gt; 데이터 변경 이력 추적(Optimistic Lock)&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;데이터 무결성 &lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장된 데이터의 정확성과 일관성을 유지하기 위한 메커니즘을 제공&lt;br /&gt;&lt;br /&gt;&lt;b&gt;제약&amp;nbsp;조건:&amp;nbsp;&lt;/b&gt;데이터&amp;nbsp;입력&amp;nbsp;시&amp;nbsp;특정&amp;nbsp;규칙&amp;nbsp;적용&amp;nbsp;(예:&amp;nbsp;기본&amp;nbsp;키,&amp;nbsp;외래&amp;nbsp;키) &lt;br /&gt;&lt;b&gt;유효성 검사:&lt;/b&gt; 데이터 형식 및 범위 확인&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;쿼리&amp;nbsp;최적화&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 검색 성능을 향상시키기 위한 기법을 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;실행&amp;nbsp;계획:&amp;nbsp;&lt;/b&gt;효율적인&amp;nbsp;쿼리&amp;nbsp;실행&amp;nbsp;방법&amp;nbsp;결정 &lt;br /&gt;&lt;b&gt;캐싱:&lt;/b&gt;&amp;nbsp;자주&amp;nbsp;사용되는&amp;nbsp;데이터나&amp;nbsp;쿼리&amp;nbsp;결과&amp;nbsp;저장&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;///////////////////////////////////////////////////&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;s&gt;무결성(integrity)은 데이터의 정확성, 일관성 및 신뢰성을 보장하는 핵심 개념으로, 데이터베이스의 품질을 유지하는 데 중요한 역할을 하고 있다.&lt;/s&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;s&gt;&lt;u&gt;접은 글 참조&lt;/u&gt;&lt;/s&gt;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;s&gt;&lt;b&gt;1.&amp;nbsp;개체&amp;nbsp;무결성&amp;nbsp;(Entity&amp;nbsp;Integrity)&lt;/b&gt; &lt;/s&gt;&lt;br /&gt;&lt;s&gt;개체&amp;nbsp;무결성은&amp;nbsp;데이터베이스의&amp;nbsp;각&amp;nbsp;레코드가&amp;nbsp;고유한&amp;nbsp;식별자를&amp;nbsp;가져야&amp;nbsp;한다는&amp;nbsp;원칙이다.&amp;nbsp;일반적으로&amp;nbsp;기본&amp;nbsp;키(primary&amp;nbsp;key)가&amp;nbsp;사용되며,&amp;nbsp;이는&amp;nbsp;각&amp;nbsp;행을&amp;nbsp;구별할&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;고유한&amp;nbsp;값이다.&amp;nbsp;기본&amp;nbsp;키는&amp;nbsp;NULL&amp;nbsp;값을&amp;nbsp;가질&amp;nbsp;수&amp;nbsp;없으며,&amp;nbsp;각&amp;nbsp;레코드는&amp;nbsp;유일해야&amp;nbsp;한다.&lt;/s&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;s&gt;예시)&lt;/s&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;s&gt;학생 데이터베이스에서 학생 테이블이 있다고 가정하였을 때, 이 테이블에서 각 학생은 고유한 학생 ID를 가지고 있다. 이 학생 ID는 기본 키로 설정되어 있으며, NULL 값을 가질 수 없다. 예를 들어, 학생 A의 ID가 1001이라면, 다른 학생의 ID는 1001이 될 수 없다. 이를 통해 각 학생 레코드가 고유하게 식별한다.&lt;/s&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;s&gt;* 그렇다면 PK가 없는 Table도 존재할까?&lt;/s&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;s&gt;경우에 따라 다를 수 있지만 Log를 관리하는 Table의 경우 타임스탬프, 사용자 ID 등과 같은 로그 내역을 기반으로 레코드를 찾는 경우가 존재할 수 있다.&lt;/s&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;s&gt;&lt;b&gt;2.&amp;nbsp;참조&amp;nbsp;무결성&amp;nbsp;(Referential&amp;nbsp;Integrity)&lt;/b&gt;&amp;nbsp;&lt;/s&gt;&lt;br /&gt;&lt;s&gt;참조 무결성은 데이터베이스의 두 테이블 간의 관계를 유지하는 원칙이다. 예를 들어, 외래 키(foreign key)는 다른 테이블의 기본 키를 참조하여 두 테이블 간의 관계를 정의한다. 참조 무결성을 유지하려면 외래 키 값은 항상 해당하는 기본 키 값이 존재해야 하며, 삭제 또는 업데이트 시 적절한 조치가 필요하다.&lt;/s&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;s&gt;예시)&amp;nbsp;&lt;/s&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;s&gt;주문 데이터베이스에서 고객 테이블과 주문 테이블이 있다고 가정할 때, 고객 테이블의 기본 키인 고객 ID가 주문 테이블의 외래 키로 사용된다. 만약 고객 ID가 2001인 고객이 주문을 했다고 하면, 주문 테이블의 해당 주문 레코드는 고객 ID 2001을 참조하게 된다. 이 경우, 고객 ID 2001이 고객 테이블에 존재해야 하며, 고객 레코드가 삭제될 경우 해당 주문 레코드도 함께 삭제되거나 NULL로 설정되어야 한다.&lt;/s&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;s&gt;&lt;b&gt;3.&amp;nbsp;도메인&amp;nbsp;무결성&amp;nbsp;(Domain&amp;nbsp;Integrity)&lt;/b&gt; &lt;/s&gt;&lt;br /&gt;&lt;s&gt;도메인 무결성은 각 데이터 필드가 특정 데이터 유형, 포맷 및 범위를 따르도록 보장한다. 예를 들어, 나이 필드는 정수형 데이터만 허용하고, 이메일 필드는 특정 포맷을 따라야 한다.&lt;/s&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;s&gt;예시)&lt;/s&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;s&gt;직원 데이터베이스에서 직원의 나이 필드가 있다고 가정하였을 때, 이 나이 필드는 정수형 데이터만 허용하고, 0에서 150 사이의 값만 입력될 수 있도록 제약 조건을 설정한다. 예를 들어, 직원의 나이를 200으로 입력하려고 하면 시스템에서 오류 메시지를 발생시켜 잘못된 데이터를 방지한다.&lt;/s&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;s&gt;&lt;b&gt;4.&amp;nbsp;사용자&amp;nbsp;정의&amp;nbsp;무결성&amp;nbsp;(User-Defined&amp;nbsp;Integrity)&lt;/b&gt; &lt;/s&gt;&lt;br /&gt;&lt;s&gt;사용자 정의 무결성은 특정 비즈니스 규칙이나 요구 사항에 따라 설정하는 무결성 제약 조건이다. 예를 들어, 재고 수량은 항상 0 이상이어야 한다는 규칙을 설정할 수 있다.&lt;/s&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;s&gt;예시)&lt;/s&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;s&gt;재고 관리 데이터베이스에서 제품 테이블이 존재할 때, 이 테이블에서 재고 수량 필드는 항상 0 이상이어야 한다는 비즈니스 규칙이 있다. 만약 재고 수량을 -5로 설정하려고 하면, 시스템에서 이를 허용하지 않도록 제약 조건을 설정한다.&lt;/s&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;DB의 유형&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스는 크게 관계형 데이터베이스(RDBMS)와 비관계형 데이터베이스(NoSQL)로 나눌 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 유형은 고유한 특징과 장단점을 가지고 있어, 사용 목적에 따라 적절한 선택이 필요하다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 231px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 14.3798%; height: 20px;&quot;&gt;유형&lt;/td&gt;
&lt;td style=&quot;width: 36.0077%; height: 20px;&quot;&gt;특징&lt;/td&gt;
&lt;td style=&quot;width: 32.7713%; height: 20px;&quot;&gt;예시&lt;/td&gt;
&lt;td style=&quot;width: 16.8411%;&quot;&gt;사례&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 120px;&quot;&gt;
&lt;td style=&quot;width: 14.3798%; height: 120px;&quot;&gt;관계형&amp;nbsp;&lt;br /&gt;데이터베이스&amp;nbsp;&lt;br /&gt;(RDBMS)&lt;/td&gt;
&lt;td style=&quot;width: 36.0077%; height: 120px;&quot;&gt;데이터를 행과 열로 구성된 테이블로 구조화&lt;br /&gt;SQL(Structured&amp;nbsp;Query&amp;nbsp;Language)을&amp;nbsp;사용하여&amp;nbsp;데이터&amp;nbsp;조작&lt;br /&gt;ACID(원자성,&amp;nbsp;일관성,&amp;nbsp;고립성,&amp;nbsp;지속성)&amp;nbsp;특성을&amp;nbsp;보장&lt;br /&gt;데이터&amp;nbsp;무결성과&amp;nbsp;일관성&amp;nbsp;유지에&amp;nbsp;강점&lt;/td&gt;
&lt;td style=&quot;width: 32.7713%; height: 120px;&quot;&gt;MySQL:&amp;nbsp;오픈소스로,&amp;nbsp;웹&amp;nbsp;애플리케이션에&amp;nbsp;널리&amp;nbsp;사용됨&lt;br /&gt;PostgreSQL:&amp;nbsp;고급&amp;nbsp;기능을&amp;nbsp;제공하는&amp;nbsp;오픈소스&amp;nbsp;DBMS&lt;br /&gt;Oracle:&amp;nbsp;엔터프라이즈급&amp;nbsp;솔루션으로&amp;nbsp;널리&amp;nbsp;사용됨&lt;br /&gt;Microsoft&amp;nbsp;SQL&amp;nbsp;Server:&amp;nbsp;윈도우&amp;nbsp;환경에서&amp;nbsp;많이&amp;nbsp;사용되는&amp;nbsp;DBMS&lt;/td&gt;
&lt;td style=&quot;width: 16.8411%;&quot;&gt;금융 거래, 고객 관리 시스템, 재고 관리 등 데이터 일관성이 중요한 업무&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 57px;&quot;&gt;
&lt;td style=&quot;width: 14.3798%; height: 57px;&quot; rowspan=&quot;4&quot;&gt;비관계형&amp;nbsp;&lt;br /&gt;데이터베이스&amp;nbsp;&lt;br /&gt;(NoSQL)&lt;/td&gt;
&lt;td style=&quot;width: 36.0077%; height: 57px;&quot;&gt;a) 문서 기반 데이터베이스&lt;br /&gt;JSON&amp;nbsp;형식과&amp;nbsp;유사한&amp;nbsp;문서로&amp;nbsp;데이터&amp;nbsp;저장&lt;br /&gt;스키마&amp;nbsp;유연성이&amp;nbsp;높음&lt;br /&gt;복잡한&amp;nbsp;계층&amp;nbsp;구조의&amp;nbsp;데이터&amp;nbsp;처리에&amp;nbsp;적합&lt;/td&gt;
&lt;td style=&quot;width: 32.7713%; height: 57px;&quot;&gt;MongoDB:&amp;nbsp;가장&amp;nbsp;널리&amp;nbsp;사용되는&amp;nbsp;문서&amp;nbsp;기반&amp;nbsp;데이터베이스&lt;br /&gt;CouchDB:&amp;nbsp;HTTP&amp;nbsp;프로토콜을&amp;nbsp;네이티브로&amp;nbsp;지원&lt;/td&gt;
&lt;td style=&quot;width: 16.8411%;&quot;&gt;콘텐츠 관리, 실시간 빅데이터, 모바일 앱 백엔드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 36.0077%; height: 17px;&quot;&gt;b) 키-값 저장소&lt;br /&gt;단순한&amp;nbsp;키-값&amp;nbsp;쌍으로&amp;nbsp;데이터&amp;nbsp;저장&lt;br /&gt;빠른&amp;nbsp;읽기와&amp;nbsp;쓰기&amp;nbsp;성능&lt;br /&gt;복잡한&amp;nbsp;쿼리나&amp;nbsp;관계&amp;nbsp;처리는&amp;nbsp;제한적&lt;/td&gt;
&lt;td style=&quot;width: 32.7713%; height: 17px;&quot;&gt;Redis:&amp;nbsp;인메모리&amp;nbsp;데이터&amp;nbsp;구조&amp;nbsp;저장소로&amp;nbsp;캐싱에&amp;nbsp;많이&amp;nbsp;사용&lt;br /&gt;DynamoDB:&amp;nbsp;AWS에서&amp;nbsp;제공하는&amp;nbsp;완전&amp;nbsp;관리형&amp;nbsp;NoSQL&amp;nbsp;데이터베이스&lt;/td&gt;
&lt;td style=&quot;width: 16.8411%;&quot;&gt;세션&amp;nbsp;관리,&amp;nbsp;장바구니,&amp;nbsp;실시간&amp;nbsp;분석&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 36.0077%; height: 17px;&quot;&gt;c) 열 지향 데이터베이스&lt;br /&gt;데이터를&amp;nbsp;열&amp;nbsp;단위로&amp;nbsp;저장&lt;br /&gt;대규모&amp;nbsp;분산&amp;nbsp;시스템에&amp;nbsp;적합&lt;br /&gt;빠른&amp;nbsp;데이터&amp;nbsp;집계와&amp;nbsp;분석&amp;nbsp;가능&lt;/td&gt;
&lt;td style=&quot;width: 32.7713%; height: 17px;&quot;&gt;Cassandra:&amp;nbsp;높은&amp;nbsp;확장성과&amp;nbsp;가용성&amp;nbsp;제공&lt;br /&gt;HBase:&amp;nbsp;Hadoop&amp;nbsp;생태계와&amp;nbsp;잘&amp;nbsp;통합됨&lt;/td&gt;
&lt;td style=&quot;width: 16.8411%;&quot;&gt;시계열&amp;nbsp;데이터,&amp;nbsp;로그&amp;nbsp;데이터&amp;nbsp;분석,&amp;nbsp;IoT&amp;nbsp;데이터&amp;nbsp;처리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 36.0077%;&quot;&gt;d) 그래프 데이터베이스&lt;br /&gt;노드와&amp;nbsp;관계를&amp;nbsp;중심으로&amp;nbsp;데이터&amp;nbsp;모델링&lt;br /&gt;복잡한&amp;nbsp;관계&amp;nbsp;분석에&amp;nbsp;탁월한&amp;nbsp;성능&lt;br /&gt;실시간&amp;nbsp;쿼리&amp;nbsp;처리에&amp;nbsp;강점&lt;/td&gt;
&lt;td style=&quot;width: 32.7713%;&quot;&gt;Neo4j:&amp;nbsp;가장&amp;nbsp;널리&amp;nbsp;사용되는&amp;nbsp;그래프&amp;nbsp;데이터베이스&lt;br /&gt;ArangoDB:&amp;nbsp;문서,&amp;nbsp;키-값,&amp;nbsp;그래프&amp;nbsp;모델을&amp;nbsp;모두&amp;nbsp;지원하는&amp;nbsp;멀티모델&amp;nbsp;데이터베이스&lt;/td&gt;
&lt;td style=&quot;width: 16.8411%;&quot;&gt;소셜&amp;nbsp;네트워크&amp;nbsp;분석,&amp;nbsp;추천&amp;nbsp;시스템,&amp;nbsp;사기&amp;nbsp;탐지&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;결론&lt;/b&gt; :&amp;nbsp;최근에는 여러 유형의 장점을 결합한 하이브리드 솔루션도 등장하고 있어, 더욱 다양한 선택지가 제공되고 있기 때문에 적절한 환경에 맞는 기술스택을 활용하는 것이 올바르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참조 링크&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://untitledtblog.tistory.com/123&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://untitledtblog.tistory.com/123&lt;/a&gt;&lt;/p&gt;</description>
      <category>DB</category>
      <author>challduck</author>
      <guid isPermaLink="true">https://challduck.tistory.com/32</guid>
      <comments>https://challduck.tistory.com/32#entry32comment</comments>
      <pubDate>Wed, 14 Aug 2024 17:44:42 +0900</pubDate>
    </item>
    <item>
      <title>상품 재고 감소의 동시성 문제</title>
      <link>https://challduck.tistory.com/30</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;동시성 문제?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 스레드가 동시에 같은 리소스에 접근할 때 발생할 수 있는 문제를 의미한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Spring Boot(Java) 환경 에서 발생하는 이유&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Java는 멀티 스레드 환경이므로 여러개의 요청이 재고의 값을 변경하려고 할 때, 정상적으로 변경되지 않는 문제가 발생한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1721645549135&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SpringBootTest
@ActiveProfiles(profiles = &quot;test&quot;)
class ProductStockServiceTest {

    @Autowired
    private ProductStockService stockService;

    @Autowired
    private ProductStockRepository stockRepository;

    @Autowired
    private ProductRepository productRepository;

    @BeforeEach
    public void createStock(){
        Product product = productRepository.save(Product.builder()
                        .name(&quot;product 1&quot;)
                        .price(1000)
                        .description(&quot;product description&quot;)
                .build());

        stockRepository.saveAndFlush(ProductStock.builder()
                        .product(product)
                        .stock(100)
                .build());
    }

    @AfterEach
    public void deleteAll(){
        stockRepository.deleteAll();
    }

    @Test
    void decrementStock() throws InterruptedException {
        int threadCount = 100;
        ExecutorService executorService = Executors.newFixedThreadPool(32);
        CountDownLatch countDownLatch = new CountDownLatch(threadCount);

        ProductStockDecrementServiceRequest request = new ProductStockDecrementServiceRequest(1L, 1);

        for (int i = 0; i &amp;lt; threadCount; i++) {
            executorService.submit(()-&amp;gt;{
                try{
                    stockService.decrementStock(request);
                } finally {
                    countDownLatch.countDown();
                }
            });
        }

        countDownLatch.await();

        ProductStock stock = stockRepository.findById(1L).orElseThrow();

        Assertions.assertThat(stock.getStock()).isEqualTo(0);
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;100개의 재고를 100개의 스레드에서 1번씩 감소시키는 테스트 코드이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 0개가 될&amp;nbsp; 것이라고 예상하지만 실제 테스트 결과는 아래와 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;995&quot; data-origin-height=&quot;292&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLa5np/btsIIBJ79pF/sKpHxmUxYPtvb7APUzxDsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLa5np/btsIIBJ79pF/sKpHxmUxYPtvb7APUzxDsK/img.png&quot; data-alt=&quot;만약 실제 서비스였다고 한다면, 재고가 0이 될 때 까지 100 이상의 요청이 정상 처리 되었을 것 이다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLa5np/btsIIBJ79pF/sKpHxmUxYPtvb7APUzxDsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLa5np%2FbtsIIBJ79pF%2FsKpHxmUxYPtvb7APUzxDsK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;995&quot; height=&quot;292&quot; data-origin-width=&quot;995&quot; data-origin-height=&quot;292&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;만약 실제 서비스였다고 한다면, 재고가 0이 될 때 까지 100 이상의 요청이 정상 처리 되었을 것 이다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선택한 해결 방법 : Redis(Redisson)를 활용한 분산락(Distribute Lock)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;선택한 이유&lt;/b&gt;&lt;br /&gt;1. 현재 프로젝트에 Redis를 적용하고 있으므로 추가적인 비용을 들이지 않고 적용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. Redisson은 분산 환경에서 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. Redisson은 DB Lock에 비해 DB 부하 없이 확장이 용이하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. Redisson은 Lettuce와 다르게 락 해제 등의 작업을 자동으로 처리해주는 기능이 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결 방안&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Syncronized&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Syncronized&lt;br /&gt;&lt;b&gt;개념&lt;/b&gt; : Synchronized는 Java에서 제공하는 동기화 메커니즘으로, 여러 스레드가 동시에 같은 리소스에 접근하는 것을 방지한다. 메소드나 블록에 synchronized 키워드를 사용하면, 한 번에 하나의 스레드만 해당 코드를 실행할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Syncronized Service 구현 코드&lt;/p&gt;
&lt;pre id=&quot;code_1721703114483&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class StockService {
    
    private final StockRepository stockRepository;

    public StockService(StockRepository stockRepository) {
        this.stockRepository = stockRepository;
    }

    @Transactional
    public synchronized void decrease(Long id, int quantity) {
        Stock stock = stockRepository.findById(id).orElseThrow();
        stock.decrease(quantity);
        stockRepository.save(stock);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;문제점 : Syncronized는 단일 JVM 내에서 동작하므로 서버가 분산 배포된 환경에서는 동시성 문제를 해결할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 한 번에 하나의 스레드만 접근할 수 있어, 다른 스레드들은 대기해야 하며 이는 전체적인 시스템 성능을 저하시킬 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;DB Lock&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Pessimistic&amp;nbsp;Lock&lt;/b&gt;&amp;nbsp;(비관적&amp;nbsp;락)&lt;br /&gt;&lt;b&gt;개념&lt;/b&gt; : 실제 데이터에 lock을 걸어 정합성을 맞춘다. &lt;br /&gt;exclusive lock을 걸게 되면 다른 트랜잭션에서 lock이 해제되기 전까지 데이터를 가져갈 수 없게 된다.&lt;br /&gt;&lt;br /&gt;구현한 코드 내용은 아래와 같다.&lt;br /&gt;&lt;br /&gt;Product Stock Service&lt;/p&gt;
&lt;pre id=&quot;code_1721668571454&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class ProductStockService {
    private final ProductStockRepository productStockRepository;
    
    @Transactional
    public void stockDecrementPessimisticLock(ProductStockDecrementServiceRequest request){
        ProductStock productStock = productStockRepository.findByIdWithPessimisticLock(request.productId());
        productStock.decrement(request.quantity());
        productStockRepository.save(productStock);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;Product Stock Repository&lt;/p&gt;
&lt;pre id=&quot;code_1721668598584&quot; class=&quot;less&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Repository
public interface ProductStockRepository extends JpaRepository&amp;lt;ProductStock, Long&amp;gt; {

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query(&quot;SELECT ps FROM ProductStock ps WHERE ps.id = :id&quot;)
    ProductStock findByIdWithPessimisticLock(@Param(&quot;id&quot;) Long id);

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;495&quot; data-origin-height=&quot;68&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0CRdq/btsIHVoJVD9/4eAGgKxj1UV1vMSH8orXp0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0CRdq/btsIHVoJVD9/4eAGgKxj1UV1vMSH8orXp0/img.png&quot; data-alt=&quot;테스트 결과는 성공이다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0CRdq/btsIHVoJVD9/4eAGgKxj1UV1vMSH8orXp0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb0CRdq%2FbtsIHVoJVD9%2F4eAGgKxj1UV1vMSH8orXp0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;495&quot; height=&quot;68&quot; data-origin-width=&quot;495&quot; data-origin-height=&quot;68&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;테스트 결과는 성공이다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그렇다면 비관적 락을 동시성 문제에 무작정 사용해도 문제가 없을까?&lt;br /&gt;&lt;br /&gt;Deadlock이 발생하는 조건은 아래와 같다.&lt;br /&gt;&lt;br /&gt;1. 순환적 대기 조건 (Circular Wait Condition)&lt;br /&gt;: 여러 트랜잭션이 자원을 잠금상태로 만들면서 서로 다른 자원을 기다릴 때 발생한다.&lt;br /&gt;(예: 트랜잭션 A에서 자원 1을 잠그고 자원 2를 기다린다. 동시에 트랜잭션 B가 자원 2를 잠그고 자원 1을 기다린다.)&lt;br /&gt;&lt;br /&gt;2. 상호 배제 조건 (Mutual Exclusion Condition)&lt;br /&gt;: 자원은 한 번에한 프로세스만 사용할 수 있어야 한다. 하나의 트랜잭션이 자원을 사용하고 있는 동안 다른 트랜잭션은 해당 자원에 접근 할 수 없어서 대기가 발생한다.&lt;br /&gt;&lt;br /&gt;3. 점유와 대기 조건 (Hold and Wait Condition)&lt;br /&gt;: 트랜잭션이 이미 적어도 하나의 자원을 점유하며 추가 자원을 기다리는 상황&lt;br /&gt;&lt;br /&gt;4. 비선점 조건 (No Preemption Condition)&lt;br /&gt;: 이미 잠긴 자원은 그 자원을 사용하는 트랜잭션이 해제할 때까지 강제로 빼앗을 수 없다.&lt;br /&gt;&lt;br /&gt;Pessimistic lock은 자원을 점유하는 동안 다른 트랜잭션의 접근을 차단하기 때문에 위 조건들이 쉽게 충족될 수 있다. 따라서 추가적으로 데드락을 회피하는 로직을 구현해야한다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;Optimistic Lock&lt;/b&gt; (낙관적 락)&lt;br /&gt;&lt;br /&gt;&lt;b&gt;개념&lt;/b&gt; : 실제로 Lock을 이용하지 않고 Version을 이용하여 정합성을 맞추는 방법이다.&lt;br /&gt;먼저 데이터를 읽은 후 update를 수행할 때 현재 내가 읽은 Version이 맞는지 확인 후 Update를 진행.&lt;br /&gt;읽은 Version에서 변동 사항이 생겼다면 application에서 다시 읽은 후 작업을 수행한다.&lt;br /&gt;&lt;br /&gt;Entity에 Version 추가&lt;/p&gt;
&lt;pre id=&quot;code_1721668610898&quot; class=&quot;less&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Entity
public class ProductStock {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

	|
    
    @Version
    private Long version;

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;Facade package를 만들고 재고 감소 로직을 구현&lt;/p&gt;
&lt;pre id=&quot;code_1721668621736&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class OptimisticLockProductStockFacade {
    private final ProductStockService productStockService;

    public void decrease(ProductStockDecrementServiceRequest request) throws InterruptedException {
        while (true){
            try {
                productStockService.stockDecrementOptimisticLock(request);
                break;
            } catch (Exception e){
                Thread.sleep(50);
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;Repository&lt;/p&gt;
&lt;pre id=&quot;code_1721668634747&quot; class=&quot;less&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Repository
public interface ProductStockRepository extends JpaRepository&amp;lt;ProductStock, Long&amp;gt; {

    @Lock(LockModeType.OPTIMISTIC)
    @Query(&quot;SELECT ps FROM ProductStock ps WHERE ps.id = :id&quot;)
    ProductStock findByIdWithOptimisticLock(@Param(&quot;id&quot;) Long id);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;496&quot; data-origin-height=&quot;66&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9N4q8/btsIIgzoy2i/XlMvsBK6gX01xwZDZw1qC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9N4q8/btsIIgzoy2i/XlMvsBK6gX01xwZDZw1qC0/img.png&quot; data-alt=&quot;테스트 결과는 성공&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9N4q8/btsIIgzoy2i/XlMvsBK6gX01xwZDZw1qC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9N4q8%2FbtsIIgzoy2i%2FXlMvsBK6gX01xwZDZw1qC0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;496&quot; height=&quot;66&quot; data-origin-width=&quot;496&quot; data-origin-height=&quot;66&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;테스트 결과는 성공&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;예시 코드는 version이 적절할 때 까지 시도하므로 실제 구현에서는 성능을 고려하여 로직을 구현해야 하며 &lt;br /&gt;이러한 특성으로 인해 읽기 작업이 쓰기 작업보다 많은 경우에 사용하기 적절하다.&lt;br /&gt;&lt;br /&gt;재시도 로직에서 구현을 잘못하면 데드락이 발생할 수 있다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;Named Lock&lt;/b&gt; (네임드 락)&lt;br /&gt;&lt;b&gt;개념&lt;/b&gt; : 이름을 가진 metadata lock. 이름을 가진 lock을 획득 후 해제할 때까지 다른 세션은 이 lock을 획득할 수 없게 한다. 주의할 점은 transaction이 종료될 때 lock이 자동으로 해제되지 않는다. 따라서 별도의 로직 또는 명령어로 해제를 수행하거나 선점시간이 끝나야 해제된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;i&gt;*만약 H2로 테스트를 진행하고 있다면 NamedLock은 지원하지 않기 때문에 별도 구현을 하거나 MySQL을 사용&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;NamedLock Interface&lt;/p&gt;
&lt;pre id=&quot;code_1721696166681&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface NamedLockRepository {
    void getLock(String productId);
    void releaseLock(String productId);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NamedLock 구현체&lt;/p&gt;
&lt;pre id=&quot;code_1721696176279&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Repository
@RequiredArgsConstructor
public class LockRepository implements NamedLockRepository {

    private final EntityManager entityManager;

    @Override
    public void getLock(String productId) {
        entityManager.createNativeQuery(&quot;SELECT GET_LOCK(?, 3000)&quot;)
                .setParameter(1, productId)
                .getSingleResult();
    }

    @Override
    public void releaseLock(String productId) {
        entityManager.createNativeQuery(&quot;SELECT RELEASE_LOCK(?)&quot;)
                .setParameter(1, productId)
                .getSingleResult();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NamedLock Facade&lt;/p&gt;
&lt;pre id=&quot;code_1721696142829&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class NamedLockProductStockFacade {

    private final LockRepository lockRepository;
    private final ProductStockService stockService;

    @Transactional
    public void decrease(final Long id, final int quantity) {
        try {
            lockRepository.getLock(id.toString());
            stockService.decrementStock(id, quantity);
        } finally {
            lockRepository.releaseLock(id.toString());
        }

    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Service&lt;/p&gt;
&lt;pre id=&quot;code_1721696293657&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class ProductStockService {
    private final ProductStockRepository productStockRepository;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void decrementStock(Long id, int quantity){
        ProductStock productStock = productStockRepository.findByProductId(id);
        productStock.decrement(quantity);
        productStockRepository.save(productStock);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;494&quot; data-origin-height=&quot;67&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/E6r0N/btsIIkhAKSp/lWPwHpFmw55nyeak9i4zNK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/E6r0N/btsIIkhAKSp/lWPwHpFmw55nyeak9i4zNK/img.png&quot; data-alt=&quot;Named Lock 테스트 결과는 성공&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/E6r0N/btsIIkhAKSp/lWPwHpFmw55nyeak9i4zNK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FE6r0N%2FbtsIIkhAKSp%2FlWPwHpFmw55nyeak9i4zNK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;494&quot; height=&quot;67&quot; data-origin-width=&quot;494&quot; data-origin-height=&quot;67&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Named Lock 테스트 결과는 성공&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NamedLock을 적용했을 때 문제점은 무엇이 있을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lock 해제를 보장하지 않는 코드는 영구적인 DeadLock을 유발한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 요청이 동시에 들어온다면 Lock 대기시간이 길어지며 응답시간이 늘어나며 성능 저하를 초래한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Rediss Lock&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Lettuce&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;개념&lt;/b&gt; : Redis를 사용하여 락을 관리한다. 락을 획득하면 다른 프로세스의 접근을 차단한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis Repository&lt;/p&gt;
&lt;pre id=&quot;code_1721696089459&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class RedisLockRepository {
    private final RedisTemplate&amp;lt;String, String&amp;gt; redisTemplate;

    public RedisLockRepository(RedisTemplate&amp;lt;String, String&amp;gt; redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public Boolean lock(Long key){
        return redisTemplate
                .opsForValue()
                .setIfAbsent(generated(key), &quot;lock&quot;, Duration.ofMillis(3_000));
    }

    public Boolean unlock(Long key){
        return redisTemplate.delete(generated(key));
    }

    private String generated(Long key) {
        return key.toString();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lettuce Facade&lt;/p&gt;
&lt;pre id=&quot;code_1721696065288&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class LettuceLockProductStockFacade {
    private final RedisLockRepository redisLockRepository;
    private final ProductStockService stockService;

    public void decrease(Long key, int quantity) throws InterruptedException{
        while (Boolean.FALSE.equals(redisLockRepository.lock(key))){
            Thread.sleep(10);
        }

        stockService.decrease(key, quantity);
        redisLockRepository.unlock(key);
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;495&quot; data-origin-height=&quot;63&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btouY7/btsIKeG1PMl/u0CZNA5kl0AQfGeZ1nHcp1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btouY7/btsIKeG1PMl/u0CZNA5kl0AQfGeZ1nHcp1/img.png&quot; data-alt=&quot;테스트 성공&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btouY7/btsIKeG1PMl/u0CZNA5kl0AQfGeZ1nHcp1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtouY7%2FbtsIKeG1PMl%2Fu0CZNA5kl0AQfGeZ1nHcp1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;495&quot; height=&quot;63&quot; data-origin-width=&quot;495&quot; data-origin-height=&quot;63&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;테스트 성공&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lettuce를 구현하였을 때 문제점?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;락 획득 시도가 실패할 경우 적절한 재시도 메커니즘이 필요하다. 락을 얻기 위해 재시도를 끝없이 반복한다면 시스템 리소스를 낭비할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;Redission&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;개념&lt;/b&gt; : 분산 락(동시에 하나의 프로세스만 리소스에 접근할 수 있도록 보장) 구현이 가능하며, Lock 을 자동으로 해제할 수 있고 동일 스레드에서 중첩 락 획득이 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redisson Config&lt;/p&gt;
&lt;pre id=&quot;code_1721699160483&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RedissonConfig {
    @Value(&quot;${spring.data.redis.host}&quot;)
    private String host;

    @Value(&quot;${spring.data.redis.port}&quot;)
    private int port;

    private static final String REDISSON_HOST_PREFIX = &quot;redis://&quot;;

    @Bean
    public RedissonClient redissonClient(){
        Config config = new Config();
        config.useSingleServer().setAddress(REDISSON_HOST_PREFIX + host + &quot;:&quot; + port);

        return Redisson.create(config);
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redisson Facade&lt;/p&gt;
&lt;pre id=&quot;code_1721699436176&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class RedissonLockProductStockFacade {

    private final RedissonClient redissonClient;
    private final ProductStockService productStockService;

    public void decrease(Long productId, int quantity) {
        RLock lock = redissonClient.getLock(&quot;product_stock_&quot; + productId);

        try {
            boolean isLocked = lock.tryLock(5, 1, TimeUnit.SECONDS);
            if (!isLocked) {
                throw new RuntimeException(&quot;Failed to acquire lock for product &quot; + productId);
            }

            productStockService.decrease(productId, quantity);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException(&quot;Lock acquisition was interrupted&quot;, e);
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;495&quot; data-origin-height=&quot;63&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TnMz1/btsIHIXCkyG/tIE3fwXwEMbgV9W72I1lV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TnMz1/btsIHIXCkyG/tIE3fwXwEMbgV9W72I1lV1/img.png&quot; data-alt=&quot;테스트 성공&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TnMz1/btsIHIXCkyG/tIE3fwXwEMbgV9W72I1lV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTnMz1%2FbtsIHIXCkyG%2FtIE3fwXwEMbgV9W72I1lV1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;495&quot; height=&quot;63&quot; data-origin-width=&quot;495&quot; data-origin-height=&quot;63&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;테스트 성공&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고 링크&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Sync, DB Lock 참조&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://everydayyy.tistory.com/167&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://everydayyy.tistory.com/167&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Deadlock 발생 조건 참조&lt;br /&gt;&lt;a href=&quot;https://velog.io/@dl_edge/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%EA%B5%90%EC%B0%A9%EC%83%81%ED%83%9C-4%EA%B0%80%EC%A7%80-%EC%A1%B0%EA%B1%B4&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@dl_edge/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%EA%B5%90%EC%B0%A9%EC%83%81%ED%83%9C-4%EA%B0%80%EC%A7%80-%EC%A1%B0%EA%B1%B4&lt;/a&gt;&lt;/p&gt;</description>
      <category>Spring Boot</category>
      <author>challduck</author>
      <guid isPermaLink="true">https://challduck.tistory.com/30</guid>
      <comments>https://challduck.tistory.com/30#entry30comment</comments>
      <pubDate>Mon, 22 Jul 2024 23:48:46 +0900</pubDate>
    </item>
    <item>
      <title>Spring Gateway 적용</title>
      <link>https://challduck.tistory.com/29</link>
      <description>&lt;h2&gt;프로젝트 개선 사항&lt;/h2&gt;
&lt;hr&gt;
&lt;h3&gt;1. 사용자 요청 Route 필요&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;프로젝트에 MSA 아키텍처를 적용하면서 사용자가 각 Service EndPoint에 직접 요청을 해야함.&lt;/li&gt;
&lt;li&gt;Service 트래픽 증가를 Scale-Out 해결할 계획. 따라서 Service의 인스턴스가 증가하면 해당 Service의 EndPoint를 전부 알고 있어야 하고 변동 내역을 설정에 추가해야 하는 불편한 부분 발생.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. JWT 유효성 검증 로직 중복&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Service가 분리되면서 로그인 사용자를 User Service에서 JWT로 관리 하는 중. User Service 외 인증이 필요한 부분에 Token을 검증해야 하는 로직이 필요함.&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;h2&gt;결론&lt;/h2&gt;
&lt;hr&gt;
&lt;h3&gt;Spring Gateway를 선택한 이유&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Spring 생태계에 적합&lt;/strong&gt;: Spring Cloud Gateway는 Spring 생태계와 자연스럽게 통합되어 기존 프로젝트와의 호환성이 높다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Netflix Eureka와의 통합&lt;/strong&gt;: Eureka를 사용하면 서비스 인스턴스의 증감에 따라 API Gateway에서 자동으로 라우팅 및 로드 밸런싱을 관리할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;중앙 집중식 JWT 검증&lt;/strong&gt;: 모든 요청이 API Gateway를 통해 처리되므로 별도의 인증 서버를 구축할 필요 없이 Gateway에서 JWT를 검증할 수 있다.&lt;/li&gt;
&lt;li&gt;Nginx도 로드밸런싱 기능을 제공하지만 Cloud Gateway의 동적 라우팅 및 유연한 Filter Chain의 장점으로 Spring Gateway를 선택했다.&lt;/li&gt;
&lt;/ol&gt;
&lt;br/&gt;

&lt;h2&gt;해결 방안&lt;/h2&gt;
&lt;hr&gt;
&lt;h3&gt;1. Service Route 필요&lt;/h3&gt;
&lt;br/&gt;

&lt;h4&gt;a. Nginx&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;특징&lt;/strong&gt;: 웹 서버로 시작하여 현재는 리버스 프록시, 로드 밸런서, HTTP 캐시 등의 역할을 수행한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;장점&lt;/strong&gt;: 높은 성능과 낮은 리소스 사용량을 자랑하며, 다양한 로드 밸런싱 알고리즘을 제공한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;단점&lt;/strong&gt;: 복잡한 라우팅 및 서비스 디스커버리 기능이 부족하며, 설정 변경 시 수동으로 업데이트해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;h4&gt;b. Gateway&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/images/spring_cloud_gateway_diagram.png&quot; alt=&quot;gateway-flow&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cloud Gateway 동작 과정&lt;/strong&gt;&lt;ol&gt;
&lt;li&gt;Client가 Spring Cloud Gateway에 Request함.&lt;/li&gt;
&lt;li&gt;Gateway Handler Mapping에서 Request에 대한 Path가 일치한다고 판단하면 Gateway Web Handler로 전달&lt;/li&gt;
&lt;li&gt;Gateway Web Handler는 Request에 특정 FilterChain을 통해 Request를 처리한다.&lt;ul&gt;
&lt;li&gt;(FIlter가 점선으로 구분된 이유는 pre filter, post filter로 나누어 지는데 즉, Filter가 Request에 대해 Proxy요청 전 후 모두 실행될 수 있기 때문)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;pre filter가 실행되고 proxy 요청이 수행되며 이후 post filter가 작동한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;특징&lt;/strong&gt;: 모든 요청을 중앙 집중식으로 관리하며, 각 서비스로 라우팅한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;장점&lt;/strong&gt;: 클라이언트는 단일 진입점으로 모든 서비스에 접근할 수 있으며, 로드 밸런싱, 인증, 모니터링, 로깅 등의 기능을 제공한다. 서비스 인스턴스 수가 변해도 클라이언트 설정을 변경할 필요가 없다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;단점&lt;/strong&gt;: 성능 병목이 발생할 수 있으며, 고가용성을 위해 추가적인 설정이 필요하다. 단일 실패 지점(Single Point of Failure)이 될 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;details&gt;
&lt;summary&gt;단일 실패 지점&lt;/summary&gt;
&lt;div markdown=&quot;1&quot;&gt;

&lt;p&gt;정의 : 단일 실패 지점은 시스템 내의 특정 요소가 고장나면 전체 시스템의 기능이 중단되는 경우를 의미. 즉, 이 구성 요소가 없거나 정상적으로 작동하지 않으면 다른 모든 구성 요소가 영향을 받아 서비스가 중단될 수 있다.&lt;/p&gt;
&lt;p&gt;만약 Gateway에서 request를 중앙 처리하는데 Gateway의 Server에 문제가 발생하면 Route를 진행하던 Service가 중단될 수 있다.&lt;/p&gt;
&lt;p&gt;따라서 모든 request를 처리하는 Gateway는 여러 서버를 클러스터로 구성하여, 하나의 서버가 실패하더라도 다른 서버가 요청을 처리할 수 있도록 하는 방식을 고려해야 한다.&lt;/p&gt;
&lt;/div&gt;
&lt;/details&gt;

&lt;br/&gt;

&lt;h3&gt;2. JWT 유효성 검사 중복&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;a. 별도 인증 서버 구축&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;특징&lt;/strong&gt;: 인증을 전담하는 서버를 구축하여 각 서비스가 이를 통해 인증한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;장점&lt;/strong&gt;: 인증 로직이 중앙 집중화되어 관리가 용이하며, 보안이 강화되고 인증 관련 로직의 재사용이 가능하다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;단점&lt;/strong&gt;: 추가적인 서버 유지보수 및 관리가 필요하며, 인증 서버가 성능 병목이 될 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;b. API Gateway에서 JWT 검증&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;특징&lt;/strong&gt;: API Gateway가 모든 요청의 JWT를 검증하여 각 서비스로 전달한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;장점&lt;/strong&gt;: 각 서비스에서 JWT 검증 로직이 중복되지 않으며, API Gateway에서 필요한 Filter를 추가하여 중앙 집중적으로 인증을 관리할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;단점&lt;/strong&gt;: Gateway에 트래픽이 과도하게 증가하면 Gateway의 요청을 분산해야 하며, JWT 검증 로직이 복잡해지면 Gateway의 성능에 영향을 미칠 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;h5&gt;구현&lt;/h5&gt;
&lt;p&gt;의존성 추가&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-gradle&quot;&gt;ext {
    set(&amp;#39;springCloudVersion&amp;#39;, &amp;quot;2023.0.2&amp;quot;)
}

dependencies {
    implementation &amp;#39;org.springframework.cloud:spring-cloud-starter-gateway&amp;#39;
    implementation &amp;#39;org.springframework.cloud:spring-cloud-starter-netflix-eureka-client&amp;#39;
}

dependencyManagement {
    imports {
        mavenBom &amp;quot;org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}&amp;quot;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;application.yml 수정&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yml&quot;&gt;
spring:
  application:
    name: gateway
  cloud:
    gateway:
      discovery:
      # eureka에 service 등록
        locator:
          enabled: true
          # service id를 소문자로 변환
          lower-case-service-id: true
          # Route 설정
      routes:
      # route를 구분하기 위한 고유 id
        - id: user_service
        # Eureka에 등록된 Service Application Name을 uri로 한다.
          uri: lb://user-service
          # Route 할 경로를 입력
          predicates:
            - Path=/api/v1/user/**, /api/v1/cart/**
            # 적용할 Filter 명시
          filters:
            - name: JWTFilter
        - id: product_service
          uri: lb://product-service
          predicates:
            - Path=/api/v1/product/**,/api/v1/stock/**
        - id: order_service
          uri: lb://order-service
          predicates:
            - Path=/api/v1/order/**
          filters:
            - name: JWTFilter
      # 기본적으로 적용할 Filter 설정
      default-filters:
        - name: GlobalFilter
          args:
            baseMessage: &amp;quot;Spring Cloud Gateway Global Filter&amp;quot;
            preLogger: true
            postLogger: true&lt;/code&gt;&lt;/pre&gt;
&lt;details&gt;
&lt;summary&gt;Global Filter 구현&lt;/summary&gt;
&lt;div markdown=&quot;1&quot;&gt;

&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

@Component
@Slf4j
public class GlobalFilter extends AbstractGatewayFilterFactory&amp;lt;GlobalFilter.Config&amp;gt; {
    public GlobalFilter(){
        super(Config.class);
    }

    @Override
    public GatewayFilter apply(Config config){
        return ((exchange, chain) -&amp;gt; {
            ServerHttpRequest request = exchange.getRequest();
            ServerHttpResponse response = exchange.getResponse();
            log.info(&amp;quot;Global Filter default Message : {}&amp;quot;, config.getBaseMessage());
            if (config.isPreLogger()){
                log.info(&amp;quot;Global filter Start : request id {}&amp;quot;, request.getId());
            }
            return chain.filter(exchange).then(Mono.fromRunnable(() -&amp;gt; {
                if (config.isPostLogger()){
                    log.info(&amp;quot;Global filter End: response code : {}&amp;quot;, response.getStatusCode());
                }
            }));
        });
    }

    @Data
    public static class Config {
        private String baseMessage;
        private boolean preLogger;
        private boolean postLogger;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/details&gt;

&lt;details&gt;
&lt;summary&gt;JWT 필터 구현&lt;/summary&gt;
&lt;div markdown=&quot;1&quot;&gt;

&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

import java.util.Map;

@Component
@Slf4j
public class JWTFilter extends AbstractGatewayFilterFactory&amp;lt;JWTFilter.Config&amp;gt; {

    @Value(&amp;quot;${jwt.secret.key}&amp;quot;)
    private String jwtSecretKey;

    @Data
    public static class Config {
        private boolean preLogger;
        private boolean postLogger;
    }

    public JWTFilter() {
        super(Config.class);
    }

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -&amp;gt; {
            ServerHttpRequest request = exchange.getRequest();
            ServerHttpResponse response = exchange.getResponse();

            if (!request.getHeaders().containsKey(HttpHeaders.AUTHORIZATION)){
                return chain.filter(exchange);
            }

            String header = request.getHeaders().getOrEmpty(HttpHeaders.AUTHORIZATION).getFirst();
            if (!header.startsWith(&amp;quot;Bearer &amp;quot;)){
                response.setStatusCode(HttpStatus.UNAUTHORIZED);
                return response.setComplete();
            }

            String token = header.substring(7);

            try {
                Claims claims = Jwts.parser()
                        .setSigningKey(jwtSecretKey)
                        .parseClaimsJws(token)
                        .getBody();

                ServerHttpRequest.Builder mutatedRequest = request.mutate();
                for (Map.Entry&amp;lt;String, Object&amp;gt; entry : claims.entrySet()) {
                    String claimKey = &amp;quot;X-Header&amp;quot; + entry.getKey();
                    String claimValue = String.valueOf(entry.getValue());
                    mutatedRequest.header(claimKey, claimValue);
                }

                request = mutatedRequest.build();
                exchange = exchange.mutate().request(request).build();
            } catch (Exception e){
                response.setStatusCode(HttpStatus.UNAUTHORIZED);
                return response.setComplete();
            }

            log.info(&amp;quot;Custom PRE filter: request id -&amp;gt; {}&amp;quot;, request.getId());
            log.info(&amp;quot;Custom PRE filter: request uri -&amp;gt; {}&amp;quot;, request.getURI());
            return chain.filter(exchange).then(Mono.fromRunnable(() -&amp;gt; {
                log.info(&amp;quot;Custom POST filter: response code -&amp;gt; {}&amp;quot;, response.getStatusCode());
            }));
        };
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/details&gt;</description>
      <category>Spring Boot</category>
      <author>challduck</author>
      <guid isPermaLink="true">https://challduck.tistory.com/29</guid>
      <comments>https://challduck.tistory.com/29#entry29comment</comments>
      <pubDate>Mon, 22 Jul 2024 19:13:05 +0900</pubDate>
    </item>
    <item>
      <title>[항해99 취업 리부트 코스 학습일지] 프로젝트 2주차 WIL</title>
      <link>https://challduck.tistory.com/28</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;이번 주 항해 취업 리부트코스에서 내가 구현한 기능은 무엇인가요?&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1. Order 기능 구현&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2. 기존 Monolith Architecture에서 MSA로 전환하기 위한 Spring Cloud Gateway, Eureka 도입&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;해당 기능을 구현하기 위해, 어떤 기술적 의사결정을 거쳤나요?&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;1. Order에는 Product, Payment ... 등 기존 Service 기능을 가져와서 사용하게 되는데, Order라는 기능에 대해 구현해 본 경험이 없어서 고민을 오래했던 것 같다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2. Service를 분리하게 된다면 각 서버에 배포가 예상된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2-1. 각 Service간 통신을 원활하게 하기 위해&lt;span&gt;&amp;nbsp;&lt;/span&gt;Spring Gateway, Eureka를 선택하였다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;위 기술들별로 각각의 장단점이 있다면 무엇인가요?&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2. Spring Gateway, Eureka의 특징&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- Spring Gateway&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Load Balance : Client의 Request를 여러 Micro Service Instance에 분산시켜 부하를 균등하게 처리할 수 있음.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Security : 기존 Project에서 Service를 분리하게 되었을 때, 인증 및 권한과 같은 보안 기능을 Gateway에서 관리 및 처리할 수 있음.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Route : 특정 Path 또는 조건에 따라 Request를 적절한 Micre Service로 route 할 수 있음.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Filter : Request, Response에 대한 적절한 처리를 하기 위한 Filter 기능을 제공&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- Eureka&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Discovery : Service가 동적으로 추가 제거 될 때, Eureka Server를 통해 각 Service의 Instance를 자동으로 등록 및 검색 가능&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;durability : 주기적인 Health Check를 통해 Service Instance의 상태를 확인하고 장애 발생 시 다른 Instance로 전환 가능&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;선택한 이유는 Springg boot에 최적화 되어있는 점과 다른 기술 (Nginx, Kong...) 에 비해 러닝커브가 낮은 편이기 때문에 프로젝트를 빠르게 진행해야하는 특성을 고려하여 선택하였다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;참고 링크 : &lt;a href=&quot;https://yoonchang.tistory.com/86&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://yoonchang.tistory.com/86&lt;/a&gt;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;이번 주 겪은 트러블 슈팅이 있다면 무엇인가요?&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1. Order 기능을 구현하고 Jmeter로 동시성 문제가 발생하는지 테스트를 진행하였다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;＊테스트 조건&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Product Stock : 100개&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;vUser : 100&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;seconds : 10&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;테스트를 진행하기 전, Product Stock은 0이 되어야 하는데 DB에 남은 개수는 8이였다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다만 Service를 분리하는 부분을 먼저 구현하고 MSA 에서 동시성 문제가 발생하는지 테스트를 다시 시도하여 이를 해결할 예정이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;이번 주 진행된 개인 프로젝트에서 얻은 인사이트는 무엇인가요?&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 Service를 분리할 때 연관관계가 있는 기능들을 한개의 Domain으로 묶으려고 했는데 기준을 잡기가 애매했다.&lt;br /&gt;팀 스터디와 멘토링을 진행하면서 다양한 의견을 수렴하여 어떤 방식으로 구성하고 분리할 것인지에 대한 도움이 되었다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>항해99</category>
      <author>challduck</author>
      <guid isPermaLink="true">https://challduck.tistory.com/28</guid>
      <comments>https://challduck.tistory.com/28#entry28comment</comments>
      <pubDate>Tue, 2 Jul 2024 23:12:56 +0900</pubDate>
    </item>
    <item>
      <title>[항해99 취업 리부트 코스 학습일지] 프로젝트 1주차 WIL</title>
      <link>https://challduck.tistory.com/27</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;항해 취업 리부트코스 커리큘럼에 있는 프로젝트 주차가 진행되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 진행하기에 앞서 3가지의 시나리오가 주어졌는데 내가 선택한 시나리오는 예약구매 이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1주차 구현&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;회원&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이메일 인증 기능 구현 (Google SMTP, Redis TTL)&lt;/li&gt;
&lt;li&gt;회원 가입 기능&lt;/li&gt;
&lt;li&gt;회원 정보 수정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 &amp;amp; 로그아웃&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JWT 로그인 기능 구현
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;advanced 기능에 전체 로그아웃 기능을 구현하기 위해서 기존에 발급한 access token은 blacklist에 추가하여 관리하게 되었다.&lt;/li&gt;
&lt;li&gt;다만 token을 server에 저장한다면 session과의 차이점이 없다고 생각이 들었다.&lt;/li&gt;
&lt;li&gt;token 자체에 권한, 필요한 정보를 담고 있어서 저장소를 거치지않고, 값을 사용할 수 있는 점이 더 나아보여서 선택하였다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Spring Security 적용 및 filter 구현으로 token 유효 검증&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 진행하면서 발생하는 문제나 진행하면서 발생할 수 있는 문제를 팀원들간에 의견을 공유하며 문제를 해결하는데 많은 방법이 있다는것을 알게되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 남은것은 스스로 방법을 찾아보고 비교해가며 내 프로젝트에 알맞은 방법을 선택하면 될 것 같다.&lt;/p&gt;</description>
      <category>항해99</category>
      <author>challduck</author>
      <guid isPermaLink="true">https://challduck.tistory.com/27</guid>
      <comments>https://challduck.tistory.com/27#entry27comment</comments>
      <pubDate>Tue, 25 Jun 2024 21:04:36 +0900</pubDate>
    </item>
    <item>
      <title>쿠키(Cookie) 세션(Session) 그리고 JWT(Json Web Token) 로그인</title>
      <link>https://challduck.tistory.com/26</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;cookie_mim.png&quot; data-origin-width=&quot;254&quot; data-origin-height=&quot;142&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cdgzci/btsH88BhwAX/fYw5kNgNUiKHyII41hICX1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cdgzci/btsH88BhwAX/fYw5kNgNUiKHyII41hICX1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cdgzci/btsH88BhwAX/fYw5kNgNUiKHyII41hICX1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcdgzci%2FbtsH88BhwAX%2FfYw5kNgNUiKHyII41hICX1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;254&quot; height=&quot;142&quot; data-filename=&quot;cookie_mim.png&quot; data-origin-width=&quot;254&quot; data-origin-height=&quot;142&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 다양한 사이트를 방문하다 보면 아래와 같은 알림을 받을 때가 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1284&quot; data-origin-height=&quot;1939&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OTzIC/btsH70dnX5E/ESbhZiWtv3oxia8WiIeDl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OTzIC/btsH70dnX5E/ESbhZiWtv3oxia8WiIeDl1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OTzIC/btsH70dnX5E/ESbhZiWtv3oxia8WiIeDl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOTzIC%2FbtsH70dnX5E%2FESbhZiWtv3oxia8WiIeDl1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;240&quot; height=&quot;519&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1284&quot; data-origin-height=&quot;1939&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사이트에서 Cookie에 대한 내용은 2가지만 설명하고 넘어가겠다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;해외 사이트에서 유독 물어보는 이유는 유럽 연합의 GDPR(General Data Protection Regulation) 때문이다.&lt;br /&gt;법적 규제로 사용자의 개인 데이터를 수집하기 전 동의를 얻는 과정이다.&lt;/li&gt;
&lt;li&gt;개인 정보 보호 : 쿠키는 사용자의 방문 기록, 로그인 정보 등등에 대한 내용을 저장할 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2번째 내용이 이 글의 중요한 주제이므로 설명하자면,&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Cookie란 무엇인가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;정의 : 쿠키는 웹 브라우저에서 저장되는 작은 데이터 파일이다. 서버가 사용자의 브라우저에 전송하며 이후 사용자가 재 방문하였을 때 쿠키에 담긴 정보를 다시 서버로 전송할 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 왜 사용하는가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정의의 내용 중 &lt;u&gt;&lt;b&gt;작은 데이터파일의 역할&lt;/b&gt;&lt;/u&gt;&amp;nbsp;이므로 쿠키에&amp;nbsp;사용자의 로그인 상태를 유지하기 위한 정보를 담고 있을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;여기까지만 듣는다면 쿠키로 사용자 정보를 관리하는구나 라고 생각할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그렇지만 쿠키에는 가장 큰 문제점이 있었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;보안문제 : 쿠키는 사용자의 브라우저에 저장되므로 민감한 정보를 저장하기에는 위험하다.&lt;br /&gt;만약 쿠키가 탈취 또는 위변조 되어 접근한다면&amp;nbsp;&lt;b&gt;사용자의 개인정보가 노출될 수 있다!&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 Session은 어떨까?&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Session은 무엇인가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;정의 : 서버&amp;nbsp;측에서&amp;nbsp;사용자&amp;nbsp;별로&amp;nbsp;상태를&amp;nbsp;관리하는&amp;nbsp;방법이다.&amp;nbsp;사용자가&amp;nbsp;웹사이트를&amp;nbsp;방문하는&amp;nbsp;동안&amp;nbsp;서버에&amp;nbsp;사용자&amp;nbsp;정보를&amp;nbsp;저장하고,&amp;nbsp;사용자가&amp;nbsp;사이트를&amp;nbsp;떠나거나&amp;nbsp;일정&amp;nbsp;시간이&amp;nbsp;지나면&amp;nbsp;세션이&amp;nbsp;종료된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션은 보안을 생각한다면 좋은 선택지라고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 세션방식을 사용하게 된다면 사용자 세션 데이터가 모두 서버에 저장된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 곧 서버의 자원을 많이 소모하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 JWT도 알아보자.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;JWT란 무엇인가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;JWT는 JSON Web Token의 약자로, JSON 객체를 사용하여 두 개체 간에 정보를 안전하게 전송하기 위한 방식이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT는 크게 세 부분으로 구성되어있다.&lt;br /&gt;Header (헤더): 헤더는 토큰의 유형(JWT)과 해싱 알고리즘(예: HMAC SHA256 또는 RSA)을 지정한다.&lt;/p&gt;
&lt;pre id=&quot;code_1719041892449&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;alg&quot;: &quot;HS256&quot;,
  &quot;typ&quot;: &quot;JWT&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;Payload (페이로드): 페이로드는 클레임(claims)을 포함하고 있으며, 클레임은 엔티티(일반적으로 사용자)와 추가 데이터를 나타내는 키-값 쌍이다. 클레임에는 등록된 클레임(registered claims), 공개 클레임(public claims), 비공개 클레임(private claims)이 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1719042166205&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;sub&quot;: &quot;1234567890&quot;,
  &quot;name&quot;: &quot;John Doe&quot;,
  &quot;iat&quot;: 1516239022
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;Signature (서명): 서명은 헤더와 페이로드를 인코딩한 후, 지정된 알고리즘을 사용하여 비밀 키로 서명한 것이다. 이는 토큰의 무결성을 검증하는 데 사용된다. &lt;br /&gt;예시:&amp;nbsp;HMAC&amp;nbsp;SHA256&amp;nbsp;알고리즘을&amp;nbsp;사용할&amp;nbsp;때는&amp;nbsp;다음과&amp;nbsp;같이&amp;nbsp;서명한다.&lt;/p&gt;
&lt;pre id=&quot;code_1719042290729&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;HMACSHA256(
  base64UrlEncode(header) + &quot;.&quot; + base64UrlEncode(payload),
  secret
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;JWT의 구조는 아래와 같은 형태로 이루어져 있다.&lt;br /&gt;&lt;br /&gt;xxxxx.yyyyy.zzzzz &lt;br /&gt;xxxxx:&amp;nbsp;Base64 Url로&amp;nbsp;인코딩 된&amp;nbsp;헤더 &lt;br /&gt;yyyyy:&amp;nbsp;Base64Url로&amp;nbsp;인코딩된&amp;nbsp;페이로드 &lt;br /&gt;zzzzz:&amp;nbsp;서명&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 JWT Token 하나로 사용자 로그인 정보를 관리할 수 있을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가능하다. 다만 Token 하나로 사용자를 관리하게 된다면 Token의 유효기간이 길어지게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유효기간이 길어진다면, Token을 가지고 있다가 Cookie처럼 토큰이 탈취되면 보안상 문제가 발생하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 더 나은 방법은?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Token을 한 개가 아닌 Refresh Token과 Access Token 2가지로 나눠서 관리하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Access Token : 사용자가 서버에 요청을 보낼 때 인증을 위해 사용된다. 일반적으로 짧은 수명을 가지게 되며 이유는 토큰이 탈취되어도 짧은 시간 내로 설정한다면 기간이 만료된 토큰으로 만들기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Refresh Token : Access Token이 만료되었을 때, 새로운 Access Token을 발급한다. Access Token에 비해 유효기간이 긴 편이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT를 활용한 로그인 인증 로직 flow는 아래와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1165&quot; data-origin-height=&quot;1057&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cU2w6y/btsH8E10YtD/jzvUA3srJfxUucOOkxhpKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cU2w6y/btsH8E10YtD/jzvUA3srJfxUucOOkxhpKK/img.png&quot; data-alt=&quot;출처 : Eon Kim velog / 주소는 아래 참조&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cU2w6y/btsH8E10YtD/jzvUA3srJfxUucOOkxhpKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcU2w6y%2FbtsH8E10YtD%2FjzvUA3srJfxUucOOkxhpKK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1165&quot; height=&quot;1057&quot; data-origin-width=&quot;1165&quot; data-origin-height=&quot;1057&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : Eon Kim velog / 주소는 아래 참조&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT를 RDBMS 또는 NoSQL에 저장하기에 디스크 기반 스토리지를 사용하여 읽기 쓰기 속도가 느리다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 사용하게 된 것이 Redis이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis에 대해 자세한 내용은 추후 정리하여 게시글을 작성할 계획이므로 간단하게 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 메모리 기반 데이터 저장소로, 빠른 읽기, 쓰기 성능을 가지고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 저장할 때 Key, Value 형태로 저장하기 때문에 복잡한 데이터 모델링이 필요하지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 TTL(Time-To-Live) 기능을 지원하기 때문에 Access Token과 Refresh Token의 유효기간을 쉽게 관리할 수 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 이점을 생각하여 Redis를 사용하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 프로젝트에서 요구하는 기능 중에서 회원의 로그아웃, 전체 기기 로그아웃을 요구하는 내용이 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현에 대해 생각해 보니 로그아웃을 처리할 때 발급한 Refresh Token을 무효화시켜도 기존에 발급한 Access Token을 어떻게 관리해야 하나 고민을 하였고 2가지의 방법을 찾았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Access Token의 유효기간을 매우 짧게 설정한다.&lt;/li&gt;
&lt;li&gt;Access Token을 블랙리스트로 관리한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2가지 방법 모두 채택하여 프로젝트에 적용하기로 하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번의 경우 보안을 위해 토큰이 탈취되어도 짧은 유효기간으로 만료가 되기에 선택하였고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2번의 경우 Token이 탈취되었거나 사용자가 로그아웃 이후 발급된 Access Token으로 접근하여도 이를 사전에 방지하기 위해 선택하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 JWT의 장점인 Stateless는..?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Stateless의 장점을 포기하여도 선택한 이유는&amp;nbsp;대규모 애플리케이션에서는 JWT의 확장성과 분산 처리의 이점을 살릴 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 소규모 애플리케이션에서는 세션 기반 인증이 더 단순하고 저렴할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 애플리케이션의 특성, 사용자 수, 서버 인프라 등을 고려하여 적절한 방식을 선택해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 출처&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@vamos_eon/JWT%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94%EA%B0%80-%EC%82%AC%EC%9A%A9%EC%B2%982&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@vamos_eon/JWT%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94%EA%B0%80-%EC%82%AC%EC%9A%A9%EC%B2%982&lt;/a&gt;&lt;/p&gt;</description>
      <category>Spring Boot/Cookie, Session, JWT</category>
      <author>challduck</author>
      <guid isPermaLink="true">https://challduck.tistory.com/26</guid>
      <comments>https://challduck.tistory.com/26#entry26comment</comments>
      <pubDate>Sat, 22 Jun 2024 17:39:44 +0900</pubDate>
    </item>
    <item>
      <title>[항해99 취업 리부트 코스 학습일지] 자료구조(2)</title>
      <link>https://challduck.tistory.com/25</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Heap,&amp;nbsp;Hash&amp;nbsp;Table.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 강의에서 Heap의 특성과 Priority Queue, Hash Table에 대해 학습하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간혹 알고리즘 문제에 최소값, 최댓값을 출력해야하는 문제들이 있었는데 우선순위 Queue를 사용하면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 주어진 문제를 해결할때 조금 더 수월하지 않았나 하는 생각이 들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 문제에 적용하기도 좋아보였고 자료구조를 알면 알수록 문제풀이를 보는 시야가 더 넓어지는것 같아서 좋습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;오늘의 질문&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Q. 오늘 진행된 강의에서 학습한 내용은 무엇인가요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A. Heap과 Hash Table에 대해 학습하였습니다. 특정 상황에서 사용할 수 있는 우선순위 Queue와 Hash 형식을 가지는 자료구조를 사용하여 문제풀이에 걸리는 시간을 단축할 수 있었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; 항해99 취업 리부트 코스를 수강하고 작성한 콘텐츠 입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://hanghae99.spartacodingclub.kr/reboot&quot;&gt;https://hanghae99.spartacodingclub.kr/reboot&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1716895734860&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;IT 커리어 성장 코스 항해99, 개발자 취업부터 현직자 코스까지&quot; data-og-description=&quot;항해99는 실무에 집중합니다. 최단기간에 개발자로 취업하고, 현직자 코스로 폭발 성장을 이어가세요. 실전 프로젝트, 포트폴리오 멘토링, 모의 면접까지.&quot; data-og-host=&quot;hanghae99.spartacodingclub.kr&quot; data-og-source-url=&quot;https://hanghae99.spartacodingclub.kr/reboot&quot; data-og-url=&quot;https://hanghae99.spartacodingclub.kr&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cNxKgE/hyV9Zkhiew/K5ENvHFQlFOSzrzgyoYrwK/img.jpg?width=1260&amp;amp;height=630&amp;amp;face=0_0_1260_630,https://scrap.kakaocdn.net/dn/bseFsn/hyV9SrV5L1/AWvFoyyxquHfOrAFs8yia1/img.jpg?width=1260&amp;amp;height=630&amp;amp;face=0_0_1260_630,https://scrap.kakaocdn.net/dn/cnmihu/hyWdrTJQIA/FI1dZVX67ZMuQ8aPEJ3rbK/img.png?width=3840&amp;amp;height=960&amp;amp;face=0_0_3840_960&quot;&gt;&lt;a href=&quot;https://hanghae99.spartacodingclub.kr/reboot&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://hanghae99.spartacodingclub.kr/reboot&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cNxKgE/hyV9Zkhiew/K5ENvHFQlFOSzrzgyoYrwK/img.jpg?width=1260&amp;amp;height=630&amp;amp;face=0_0_1260_630,https://scrap.kakaocdn.net/dn/bseFsn/hyV9SrV5L1/AWvFoyyxquHfOrAFs8yia1/img.jpg?width=1260&amp;amp;height=630&amp;amp;face=0_0_1260_630,https://scrap.kakaocdn.net/dn/cnmihu/hyWdrTJQIA/FI1dZVX67ZMuQ8aPEJ3rbK/img.png?width=3840&amp;amp;height=960&amp;amp;face=0_0_3840_960');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;IT 커리어 성장 코스 항해99, 개발자 취업부터 현직자 코스까지&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;항해99는 실무에 집중합니다. 최단기간에 개발자로 취업하고, 현직자 코스로 폭발 성장을 이어가세요. 실전 프로젝트, 포트폴리오 멘토링, 모의 면접까지.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;hanghae99.spartacodingclub.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>항해99</category>
      <author>challduck</author>
      <guid isPermaLink="true">https://challduck.tistory.com/25</guid>
      <comments>https://challduck.tistory.com/25#entry25comment</comments>
      <pubDate>Thu, 6 Jun 2024 22:45:51 +0900</pubDate>
    </item>
    <item>
      <title>[항해99 취업 리부트 코스 학습일지] 자료구조 (1)</title>
      <link>https://challduck.tistory.com/24</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;Stack Queue Deque.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 학습한 강의 주제는 위 제목과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LIFO(Last In First Out) 형태의 Stack&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FIFO(First In First Out) 형태의 Queue&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;양방향 Queue 형태의 Deque&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 강의를 듣기 전에 CS 공부를 하다가 세개의 자료구조 이론은 이해하고 있었지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알고리즘 문제풀이에 사용해본적이 없어서 어떻게 사용해야 하나 고민하고 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항해 커리큘럼을 진행하며 알고리즘 문제가 주어졌는데 이 문제를 직접 풀어보면서 기본적인 개념을 직접 사용해보며 익히고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 방식으로 사용하면 좋을지 알고리즘 문제 풀이에 많은 도움이 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 학습한 자료구조를 통해서 알고리즘 문제 풀이를 시도할 때 더 넓은 시야로 볼 수 있어서 좋았습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;오늘의 질문&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Q.&lt;span&gt;&lt;span&gt; &lt;span&gt;오늘 진행된 강의에서 학습한 내용은 무엇인가요?&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;A. 자료구조 3가지에 대해 학습하였습니다. 기본 개념을 익히는 문제도 여럿 풀어보며 기초를 다지고, 심화 문제에서는 어떤 방식으로 문제풀이를 하면 좋을지 고민하며 문제를 다양한 방법으로 풀 수 있는 기회가 되서 좋았습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; 항해99 취업 리부트 코스를 수강하고 작성한 콘텐츠 입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://hanghae99.spartacodingclub.kr/reboot&quot;&gt;https://hanghae99.spartacodingclub.kr/reboot&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1716895734860&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;IT 커리어 성장 코스 항해99, 개발자 취업부터 현직자 코스까지&quot; data-og-description=&quot;항해99는 실무에 집중합니다. 최단기간에 개발자로 취업하고, 현직자 코스로 폭발 성장을 이어가세요. 실전 프로젝트, 포트폴리오 멘토링, 모의 면접까지.&quot; data-og-host=&quot;hanghae99.spartacodingclub.kr&quot; data-og-source-url=&quot;https://hanghae99.spartacodingclub.kr/reboot&quot; data-og-url=&quot;https://hanghae99.spartacodingclub.kr&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cNxKgE/hyV9Zkhiew/K5ENvHFQlFOSzrzgyoYrwK/img.jpg?width=1260&amp;amp;height=630&amp;amp;face=0_0_1260_630,https://scrap.kakaocdn.net/dn/bseFsn/hyV9SrV5L1/AWvFoyyxquHfOrAFs8yia1/img.jpg?width=1260&amp;amp;height=630&amp;amp;face=0_0_1260_630,https://scrap.kakaocdn.net/dn/cnmihu/hyWdrTJQIA/FI1dZVX67ZMuQ8aPEJ3rbK/img.png?width=3840&amp;amp;height=960&amp;amp;face=0_0_3840_960&quot;&gt;&lt;a href=&quot;https://hanghae99.spartacodingclub.kr/reboot&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://hanghae99.spartacodingclub.kr/reboot&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cNxKgE/hyV9Zkhiew/K5ENvHFQlFOSzrzgyoYrwK/img.jpg?width=1260&amp;amp;height=630&amp;amp;face=0_0_1260_630,https://scrap.kakaocdn.net/dn/bseFsn/hyV9SrV5L1/AWvFoyyxquHfOrAFs8yia1/img.jpg?width=1260&amp;amp;height=630&amp;amp;face=0_0_1260_630,https://scrap.kakaocdn.net/dn/cnmihu/hyWdrTJQIA/FI1dZVX67ZMuQ8aPEJ3rbK/img.png?width=3840&amp;amp;height=960&amp;amp;face=0_0_3840_960');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;IT 커리어 성장 코스 항해99, 개발자 취업부터 현직자 코스까지&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;항해99는 실무에 집중합니다. 최단기간에 개발자로 취업하고, 현직자 코스로 폭발 성장을 이어가세요. 실전 프로젝트, 포트폴리오 멘토링, 모의 면접까지.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;hanghae99.spartacodingclub.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>항해99</category>
      <author>challduck</author>
      <guid isPermaLink="true">https://challduck.tistory.com/24</guid>
      <comments>https://challduck.tistory.com/24#entry24comment</comments>
      <pubDate>Wed, 5 Jun 2024 22:50:50 +0900</pubDate>
    </item>
  </channel>
</rss>