본문 바로가기
스터디정리방

[기록] 220119

by 공부중중 2022. 1. 25.

1. 스프링 핵심 기술의 응용

-OXM이란
: XML과 자바 오브젝트를 매핑해서 상호 변환해주는 기술을 OXM(Object-XML Mapping) 이라고 합니다.

-ORM이란
: 객체와 관계형 데이터베이스의 데이터를 자동으로 매핑(연결)해주는 것을 ORM(Object-Relational Mapping) 이라고 합니다.
-> JPA(인터페이스), Hibernate(jpa의 구현체), Querydsl(정적 타입을 이용해서 SQL과 같은 쿼리를 생성할 수 있도록 해 주는 비표준 오픈소스 프레임워크로 JPQL을 편하게 작성할 수 있게 만든 빌더 클래스 모음)

-MyBatis
: 개발자가 지정한 SQL, 저장프로시저 그리고 몇가지 고급 매핑을 지원하는 퍼시스턴스 프레임워크 Java Persistence Framework 중 하나 입니다. 퍼시스턴스 프레임워크란 데이터의 저장, 조회, 수정, 삭제를 다루는 클래스 및 설정파일들의 집합을 말합니다.

-JDBC
: Java Database Connectivty 의 약자로 자바에서 데이터베이스 프로그래밍을 하기 위해 사용하는 API로써, JDBC드라이버를 연동하여 데이버베이스에 접근이 가능하다.

-JMS
: Java Message Service로써 비동기식 메시징을 위한 표준 API이다 목적지로는 큐와 토픽 모델이 있다.

 

인터페이스 분리 원칙

: 인터페이스를 사용하는 클라이언트 기준으로 분리해야 되는 원칙.[예제참고]
Vehicle, Bicycle, Flight, Car method 예제 생각.
Vehicle 인터페이스 하나로 구현한다면, drive, fly, ride를 전부 구현해야해서 원칙에 위반된다.
AbstractBicycle, AbstractFlight, AbstractCar 역할별로 인터페이스를 만들어서 구분해서 구현 하는 방법이 있다.

ConcurrentHashMap 사용하기

일반적인 HashMap은 멀티쓰레드 환경에서 의도치 않은 결과가 발생할 수 있다. Collections.synchronizedMap()과 같이 외부에서 동기화해주는 메서드가 개발되어 있지만 모든 작업을 동기화하게 되면 많은 요청이 몰릴 때 성능 하락은 피할 수 없다. 대신 동기화된 해시데이터 조작에 최적화된 ConcurrentHashMap이 대안이 될 수 있다.

전체 데이터에 락을 걸지 않는다.
읽기 작업엔 락을 사용하지 않는다.
동시성에 대한 테스트는 작성하기가 매우 어렵다. 대신 ConcurrentHashMap을 이용한 SqlRegistry 구현을 우선 만들고 테스트할 수 있다.

내장형 데이터베이스 그러나 ConcurrentHashMap은 변경이 자주 일어나는 환경에선 동기화의 성능하락에서 자유로울 수 없다. 그래서 SQL을 담는 DB같은 것을 설계해볼 순 있지만, 관계형 데이터베이스 스키마를 구현하는 것은 배보다 배꼽이 더 커질 수가 있다. 그래서 내장형 DB(H2등)를 고려해볼 수 있다.

@Sql("/generate_schema.sql")
public class MyTest {
}

내장형 DB(Embedded DB)는 인메모리 DB라고 생각하면 좋다. Persistence는 보장되지 않지만 메모리에 저장되어 빠른 IO가 가능하다. 또한 등록, 수정, 검색, 격리수준, 트랜잭션, 최적화된 락킹 등 DB가 제공할 수 있는 것들은 모두 제공할 수 있다. SQL문으로 질의가 가능한 것은 덤이다.

-추가학습
concurrent패키지에 존재하는 컬랙션들은 락을 사용할 때 발생하는 성능 저하를 최소한으로 만들어두었습니다. 락을 여러 개로 분할하여 사용하는 Lock Striping 기법을 사용하여 동시에 여러 스레드가 하나의 자원에 접근하더라도 동시성 이슈가 발생하지 않도록 도와줍니다.

 

 int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {

ConcurrentHashMap은 내부적으로 여러개의 락을 가지고 해시값을 이용해 이러한 락을 분할하여 사용합니다. 분할 락을 사용하여 병렬성과 성능이라는 두 마리의 토끼를 모두 잡은 컬렉션이라고 볼 수 있겠네요. 내부적으로 여러 락을 사용하지만 실제 구현이 어떻게 되어 있는지 자세히 알 필요는 없습니다. 일반적인 map을 사용할 때 처럼 구현하면 내부적으로 알아서 락을 자동으로 사용해줄테니 편리하게 사용할 수 있습니다.

 

2. 빈 생명주기 콜백 시작

데이터베이스 커넥션 풀이나 네트워크 소켓처럼 애플리케이션 시점에 필요한 연결을 미리 해두고, 애플리케이션 종료 시점에 연결을 모두 종료하는 작업을 진행하려면 객체의 초기화와 종료 작업이 필요하다.

 

객체가 초기화되었을 때 해야하는 작업, 객체가 죽기 직전에 안전하게 메소드를 호출하는 방법 등 의 작업이 필요하다.

스프링 빈은 간단하게 다음과 같이 라이프사이클을 가진다.

 

객체성성 → 의존관계 주입

 

스프링 빈은 객체를 생성하고, 의존관계 주입이 끝난 다음에 필요한 데이터를 사용할 수 있는 준비가 완료된다.

  • 스프링은 의존관계 주입이 완료되면 스프링 빈에게 콜백 메소드를 통해서 초기화 시점을 알려주는 다양한 기능 제공
  • 스프링은 스프링 컨테이너가 종료되기 직전에 소멸 콜백을 준다.

 

스프링 빈의 이벤트 라이프 사이클 (싱글톤)

  1. 스프링 컨테이너 생성
  2. 스프링 빈 생성 (Constructor DI)
  3. 의존관계 주입 (setter , Field DI)
  4. 초기화 콜백
  5. 사용
  6. 소멸 전 콜백
  7. 스프링 종료
  •  초기화 콜백  : 빈이 생성되고, 빈의 의존관계 주입이 완료된 후에 호출
  •  소멸전 콜백  : 빈이 소멸되지 직전에 호출
💡 객체의 생성과 초기화를 분리하자.
생성자는 필수 정보(파라미터)를 받고, 메모리를 할당해서 객체를 생성하는 책임을 가진다. 반면에 초기화는 이렇게 생성된 값들을 활용해서 외부 커넥션을 연결하는 등 무거운 동작을 수행한다. 따라서 생상자 안에서 무거운 초기화 작업을 함꼐 하는 것 보다는 객체를 생성하는 부분과 초기화 하는 부분을 명확하게 나누는 것이 유지보수 관점에서 좋다. 물론 초기화 작업이 내부 값들만 약간 변경하는 정도로 단순한 경우엔 생성자에서 한번에 다 처리하는 게 나을 수도 있다.
💡 싱글톤 빈들은 스프링 컨테이너가 종료될 때 싱글톤 빈들도 함께 종료되기 때문에 스프링 컨테이너가 종료되기 직전에 소멸 전 콜백이 일어난다. 싱글톤 처럼 컨테이너의 시작과 종료까지 생존하는 빈도 있지만, 생명주기가 짧은 빈들도 있는데 이 빈들은 컨테이너와 무관하게 해당 빈이 종료되기 직전에 소멸전 콜백이 일어난다.

스프링의 빈 생명주기 콜백 지원

  1. 인터페이스 (InitializingBean, DisposableBean)
  2. 설정 정보에 초기화 메소드, 종료 메소드 지정
  3. @PostConstruc ,@PreDestory 애노테이션 지원

3. 인터페이스 (InitializingBean, DisposableBean)

 

  • InitializingBean 은 afterPropertiesSet() 메서드로 초기화를 지원한다.
  • DisposableBean 은 destroy() 메서드로 소멸을 지원한다.
  • 단점
    • 스프링 전용 인터페이스로 스프링에게 의존한다.
    • 초기화, 소멸 메소드의 이름 변경이 안된다.
    • 코드 수정이 되지 않는 외부 라이브러리에 적용할 수 없다.
  • 2003년에 나온 것으로 현재는 거의 사용하지 않는다.
public class NetworkClient implements InitializingBean, DisposableBean {

    private String url;

    public NetworkClient() {
      System.out.println("생성자 호출, url = " + url);
    }

    public void setUrl(String url) {
      this.url = url;
    }

    //서비스 시작시 호출
    public void connect() {
      System.out.println("connect: " + url);
    }

    public void call(String message) {
      System.out.println("call: " + url + " message = " + message);
    }

    //서비스 종료시 호출
    public void disConnect() {
      System.out.println("close + " + url);
    }

    @Override
    public void afterPropertiesSet() throws Exception {
      connect();
      call("초기화 연결 메시지");
    }

    @Override
    public void destroy() throws Exception {
      disConnect();
    }
  }

 

4. 설정 정보에 초기화 메소드, 종료 메소드 지정

: 설정 정보에 @Bean(initMethod = "init", destroyMethod = "close") 처럼 초기화, 소멸 메서드를 지정할 수 있다.

  • 장점
    • 스프링 빈이 스프링 코드에 의존하지 않는다.
    • 메소드 이름을 자유롭게 줄 수 있다.
    • 코드가 아닌 설정 정보를 사용하기 때문에 코드 수정이 되지 않는 외부 라이브러리에도 초기화, 종료 메소드를 적용할 수 있다.

특징

  • destroyMethod = "close" 의 속성의 특징

 

  • 기본값이 “inferred” (추론) 으로 되어있다.
  • 알아서 스프링 빈으로 등록되어 있다면 close() 또는 shutdown() 이름의 메소드를 추론해서 호출해준다.
  • 추론 기능을 사용하지 않으려면 destroyMethod = ""빈 공백을 적어주면 된다.
@Configuration
static class LifeCycleConfig {

 @Bean(initMethod = "init", destroyMethod = "close") // 시작, 종료 지정
 public NetworkClient networkClient() {

	 NetworkClient networkClient = new NetworkClient();
	 networkClient.setUrl("<http://hello-spring.dev>");

 return networkClient;
 }

}

5. @PostConstruc ,@PreDestory 애노테이션 지원

  • @PostConstruct ,@PreDestroy 이 두 애노테이션을 사용하면 가장 편리하게 초기화와 종료를 실행할 수 있다.
  • 스프링에서도 권장하는 방법
  • javax.annotation.PostConstruct에 속해있는 어노테이션으로 스프링에 종속된 기술이 아니다.
  • 컴포넌트 스캔과 잘 어울린다.
  • 단점 : 외부 라이브러리에 적용하지 못한다. (@Bean(~~))을 이용하자.
@PostConstruct
public void init() {

	System.out.println("NetworkClient.init");
	connect();
	call("초기화 연결 메시지");

}

@PreDestroy
public void close() {

	System.out.println("NetworkClient.close");
	disConnect();
	
}

정리

  • @PostConstruc ,@PreDestory 애노테이션을 사용하자.
  • 외부 라이브러리의 초기화와 종료를 사용할 땐 @Bean(initMethod = "init", destroyMethod = "close")을 사용하자.

6. 인터페이스 분리 원칙(Interface Segregation Principle, ISP)

인터페이스 분리 원칙이란 클라이언트가 자신이 이용하지 않는 메소드에는 의존하지 않아야 한다는 원칙이다. 또는 클라이언트는 자신이 사용하는 메소드에만 의존해야 한다는 원칙이다.

인터페이스는 그 인터페이스를 사용하는 클라이언트를 기준으로 분리해야 한다고 할 수 있다.

이 인터페이스 분리 원칙은 큰 덩어리의 인터페이스들을 구체적이고 작은 단위들로 분리시킴으로써 클라이언트들이 꼭 필요한 메소드들만 이용할 수 있게 한다. 이렇게 작은 단위들로 분리시킨 인터페이스를 역할 인터페이스라고 한다.

인터페이스 분리 원칙을 위반하는 클라이언트가 사용하지 않는 메소드들까지 의존하는 구조가 있다고 가정했을 때, 클라이언트가 사용하지 않는 메소드들에 변경이 일어난다면 이 클라이언트를 재컴파일하여 배포해야 한다는 문제가 생기게 된다.

그래서 클라이언트가 사용하는 기준으로 작은 단위의 인터페이스들로 분리해서 클라이언트들이 꼭 필요한 메소드들만 이용할 수 있게 하는 인터페이스 분리 원칙을 잘 지킨다면 클래스에 필요한 메소드만 선언할 수 있고, 재사용성이 높아진다. 또 용도가 명확한 인터페이스를 제공할 수 있고, 시스템 내부의 결합도가 약화되며 리팩토링, 수정, 재배포를 쉽게 할 수 있게 된다.

정리를 하면 인터페이스 분리 원칙은 클라이언트를 기준으로 인터페이스를 분리함으로써, 클라이언트로부터 발생하는 인터페이스의 여파가 다른 클라이언트에 미치는 영향을 최소화하는 것을 목표로 한다고 할 수 있다.

 

7. 빌더 패턴(Builder Pattern)

빌더 패턴은 복잡한 객체를 생성하는 클래스와 표현하는 클래스를 분리하여 동일한 절차에서도 서로 다른 표현을 생성하는 방법을 제공한다.

 

GoF 디자인 패턴 중 생성 패턴에 해당한다. (생성 패턴: 싱글톤 패턴, 팩토리 패턴, 추상 팩토리 패턴)

생성 패턴은 인스턴스를 만드는 추상화하는 패턴이다. 생성 패턴에 속하는 패턴들은 객체를 생성, 합성하는 방법이나 객체의 표현 방법을 시스템과 분리해준다.

생성 패턴은 시스템이 상속(inheritance)보다 복합(composite) 방법을 사용하는 방향으로 진화되어 가면서 더 중요해지고 있다.

생성 패턴에서는 중요한 두가지 이슈가 있다.

1.     생성 패턴은 시스템이 어떤 구현 클래스(Concrete Class)를 사용하는지에 대한 정보를 캡슐화한다.

2.     생성 패턴은 이들 클래스의 인스턴스들이 어떻게 만들고 어떻게 결합하는지에 대한 부분을 완전히 가려준다.

정리하자면 생성 패턴을 이용하면 무엇이 생성되고, 누가 이것을 생성하며, 이것이 어떻게 생성되는지, 언제 생성할 것인지 결정하는데 유연성을 확보할 수 있게 된다.

 

팩토리 패턴이나 추상 팩토리 패턴에서 생성해야 하는 클래스에 대한 속성 값이 많을 때 문제점들이 있다.

1.     클라이언트 프로그램으로부터 팩토리 클래스로 많은 파라미터를 넘겨줄 때 타입, 순서 등에 대한 관리가 어려워져 에러가 발생할 확률이 높아진다.

2.     경우에 따라 Optional한 파라미터 중에 필요 없는 파라미터들에 대해서 팩토리 클래스에 일일이 null 값을 넘겨줘야 한다.

3.     생성해야 하는 서브 클래스가 무거워지고 복잡해짐에 따라 팩토리 클래스 또한 복잡해지다.

 

빌더 패턴은 이러한 문제들을 해결하기 위해 별도의 Builder 클래스를 만들어 필수 값에 대해서는 생성자를 통해, 선택적인 값들에 대해서는 메소드를 통해 step-by-step으로 값을 입력 받은 후에 build() 메소드를 통해 최종적으로 하나의 인스턴스를 리턴하는 방식이다.

빌더 패턴을 구현하는 방법

1.     빌더 클래스를 Static Nested Class로 생성한다. 이 때, 관례적으로 생성하고자 하는 클래스 이름 뒤에 Builder를 붙인다. 예를 들어, Computer 클래스에 대한 빌더 클래스의 이름은 ComputerBuilder라고 정의한다.

2.     빌더 클래스의 생성자는 public으로 하며, 필수 값들에 대해 생성자의 파라미터로 받는다.

3.     Optional한 값들에 대해서는 각각의 속성마다 메소드로 제공하고, 이때 중요한 것은 메소드의 리턴 값이 빌더 객체 자신이어야 한다.

4.     마지막 단계로 빌더 클래스 내에 build() 메소드를 정의하여 클라이언트 프로그램에게 최종 생성된 결과물을 제공한다. 이렇듯 build() 메소드를 통해서만 객체 생성을 제공하기 때문에 생성 대상이 되는 클래스의 생성자는 private으로 정의해야 한다.

8. 자바 직렬화 (Serialization)

자바 직렬화란 무엇인가?

자바 시스템 내부에서 사용되는 객체 또는 데이터를 외부의 자바 시스템에서도 사용할 수 있도록 바이트(byte) 형태로 데이터 변환하는 기술과 바이트로 변환된 데이터를 다시 객체로 변환하는 기술(역직렬화)을 아울러 말한다.

어떻게 사용하는가?

직렬화 조건 : 자바 기본(primitive) 타입과 java.io.Serializable 인터페이스를 상속받은 객체는 직렬화 할 수 있는 기본 조건을 가진다.

//직렬화할 회원 클래스
public class Member implements Serializable {
    private String name;
    private String email;
    private int age;
    public Member(String name, String email, int age) {
        this.name = name;
        this.email = email;
        this.age = age;
    }
    //Getter 생략
    @Override
    public String toString() {
        return String.format("Member", name, email, age);
    }
}

 

직렬화 방법 : java.io.ObjectOutputStream 객체를 이용. writeObject 메소드 사용. 역직렬화 시에는 readObject 사용.

Member member = new Member("김배민", "deliverykim@baemin.com", 25);
    byte[] serializedMember;
    try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
        try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
            //직렬화 수행
            oos.writeObject(member);
            //serializedMember -> 직렬화된 member 객체. 저장된 내용이 바이트 배열로 반환됨.
            serializedMember = baos.toByteArray();
        }
    }
    //바이트 배열로 생성된 직렬화 데이터를 base64로 변환
    System.out.println(Base64.getEncoder().encodeToString(serializedMember));
}

위 예제에서 객체를 직렬화하여 바이트 배열(byte[]) 형태로 변환하였다.

직렬화는 어떠한 객체 정보를 파일로 따로 저장을 하거나, 네트워크 상에서 주고받을때 용이하게 한다.

왜 사용하는가?

다른 데이터 직렬화 ex) csv, json 등

{ 
  name: "김배민",
  email: "deliverykim@baemin.com",
  age: 25 
}
Member member = new Member("김배민", "deliverykim@baemin.com", 25);
//member객체를 json으로 변환 
String json = String.format("", member.getName(), member.getEmail(), member.getAge());

자바 직렬화 형태의 데이터 교환은 자바 시스템 간의 데이터 교환을 위해서 존재한다.

9. 마샬링(Marshalling)

마샬링은 직렬화와 비슷한 개념이다. 다른 점이라면 직렬화는 ‘byte stream으로 변환‘을 하는 것이지만, 마샬링은 ‘변환하는 일련의 과정‘을 뜻한다. 그러므로 마샬링이 좀 더 큰 개념이다.

마샬링은 직렬화와 거의 비슷한데 다른 점은 코드베이스를 포함하여 객체를 직렬화 한다.

🤔 코드베이스?

코드베이스는 객체를 받는이에게 이 객체의 구현을 어디서 찾을 수 있는지에 관한 정보를 말한다. 만약 다른 프로그램에게 객체를 전달했는데 이 객체를 다시 사용하려면 데이터 타입에 관한 정보가 있어야 한다. 코드베이스는 이 데이터가 어디에 있는지 알려주는 정보이다.

코드베이스가 있기 때문에, 객체의 정보를 알지 못하는 다른 JVM에게 마샬링을 통해서 건네주면, 그 JVM이 언마샬링을 통해, 객체를 원래 상태로 되돌릴 수 있다.

10. 직렬화와 마샬링의 차이

직렬화와 마샬링은 거의 비슷하게 사용된다. 위와 같이 마샬링이 직렬화보다 더 큰 범위를 가지며, 직렬화는 마샬링의 한 부분이다.

직렬화와의 가장 큰 차이점은 직렬화는 객체가 대상이지만 마샬링은 변환 자체에 목적이 있다.

마샬링은 추가적인 메타 데이터(코드 베이스)를 가질 수 있다는 것에서 직렬화와 구별된다.

 

11. Resource

특징

  • java.net.URL을 추상화한 것
  • 스프링 내부에서 많이 사용하는 인터페이스

추상화 이유

  • 클래스패스 기준으로 리소스 읽어오는 기능 부재
  • ServletContext를 기준으로 상대 경로로 읽어오는 기능 부재
  • 새로운 핸들러를 등록하여 특별한 URL 접미사를 만들어 사용할 수는 있지만 구현이 복잡하고 편의성 메소드가 부족하다.

구현체

  • UrlResource : 기본으로 지원하는 프로토콜 http, https, ftp, file, jar.
  • ClassPathResource : 지원하는 접두어 classpath:
  • FileSystemResource
  • ServletContextResource : 웹 애플리케이션 루트에서 상대경로로 리소스를 찾는다.
    ...

리소스 읽어오기

  • Resource의 타입은 location 문자열과 ApplicationContext의 타입에 따라 결정된다.
    • ClassPathXmlApplicationContext ⇒ ClassPathResource
    • FileSystemXmlApplicationContext ⇒ FileSystemResource
    • WebApplicationContext ⇒ ServletContextResource
  • ApplicationContext의 타입에 상관없이 리소스 타입을 강제하려면 java.net.URL 접두어(+classpath:) 중 하나를 사용할 수 있음
    • classpath:me/corn/config.xml ⇒ ClassPathResource
    • file://some/resource/path/config.xml ⇒ FileSystemResource

 

12. 오토와이어링의 모호성

오토와이어링의 단점은 일치하는 빈이 여럿 있을때 사용하기 힘들다는 단점이 있다.

 

@Component
public class Cake implements Dessert{. . .}
    
@Component
public class Cookie implements Dessert{. . .}
    
@Component
public class IceCream implements Dessert{. . .}

이런식으로 구현이 되어 있을 때, setDessert() 메소드를 사용해 @Autowired 한다면, 스프링은 오토와이어링의 클래스를 선택할 수 없으며, 실패하고 예외를 발생시킨다.

 

기본 빈 지정

빈 선언시, @Primary 애노테이션을 @Component 또는 @Bean과 함께 사용하면, 여러 후보 빈중에서 해당 빈을 주요 빈으로 선택할 수 있다.

 

또한, XML에서 primary 애트리뷰트를 사용하여 기본 빈을 지정할 수 있다.

<bean id="iceCream" class="com.dessert.IceCream" primary="true"></bean>

하지만 @Primary 애노테이션을 여러 개 사용한다면 새로운 모호성이 발생한다.

 

오토와이어링 빈의 자격

기본 빈의 한계점은 @Primary가 하나의 명백한 옵션 선택을 하지 못한다는 점이다.

모든 한정자에 적용한 후에도 모호함이 남아있는 경우, 항상 새로운 범위를 좁히기 위해 다시 많은 수식자를 적용한다.

 

이때 @Qualifier 애노테이션을 사용하여 수식자를 사용하면 된다.

@Autowired
@Qualifier("iceCream")
public void setDessert(Dessert dessert){
    this.dessert = dessert;
}

수식자로 빈 ID에 의존하는 대신, 빈에 자신의 수식자를 지정하는 것이다.

 

 

 

 

 

 

References

스프링이론기초 oxm, orm
JNDI, JTA, JTS, JMS
JPA와 MYBATIS 차이
자바 퍼시스턴스 프레임워크란
Spring Data Jpa, Jpa, Hibernate 차이
우아한형제들의 QueryDsl 사용법
PostConstruct(DI까지 완료 후 초기화)와 빈 생명주기 메서드와 실행순서
인터페이스 분리 원칙 - ISP
ConcurrentHashMap이란
Multi Thread 환경에서 동시성 제어하는 방법
토비스프링정리

[생성 패턴] 빌더 패턴(Builder pattern) 이해 예제 :: 준비된 개발자 (tistory.com)

- 김영한님의 스프링 핵심원리 - 기본편 (인프런)

직렬화란 무엇일까?

직렬화 vs 마샬링

마샬링과 시리얼라이즈의 차이

자바 직렬화 훑어보기

직렬화와 마샬링

[스프링프레임워크] 오토와이어링의 모호성

https://cornswrold.tistory.com/413[Spring] ResourceLoader, Resource 추상화

 

'스터디정리방' 카테고리의 다른 글

[기록] 220126  (0) 2022.02.03
[기록] 220112  (0) 2022.01.16
[기록] 220105  (0) 2022.01.09
[기록] 211229  (0) 2022.01.02
[기록] 211222  (0) 2021.12.26

댓글