http://www.corej2eepatterns.com/SessionFacade.htm

 

자바 성능 튜닝 이야기 책에서 말한 핵심 디자인 패턴을 정리해보려고 한다. 

 

Business Delegate

Business Delegate를 사용하여 비즈니스 서비스에 대한 액세스를 캡슐화. Business Delegate는 조회 및 액세스 메커니즘과 같은 비즈니스 서비스의 구현 세부 정보를 숨깁니다.

 

Problem

비즈니스 서비스 구성 요소와의 원격 통신의 복잡성으로부터 클라이언트를 숨기고 싶습니다.

Forces

  1. 장치, 웹 서비스 및 리치 클라이언트와 같은 프레젠테이션 계층 구성 요소 및 클라이언트에서 비즈니스 계층 구성 요소에 액세스하려고 합니다.
  2. 클라이언트와 비즈니스 서비스 간의 결합을 최소화하여 조회 및 액세스와 같은 서비스의 기본 구현 세부 정보를 숨기려고 합니다.
  3. 원격 서비스의 불필요한 호출을 피하려고 합니다.
  4. 네트워크 예외를 애플리케이션 또는 사용자 예외로 변환하려고 합니다.
  5. 클라이언트에서 서비스 생성, 재구성 및 호출 재시도에 대한 세부 정보를 숨기려고 합니다.

Consequences

커플링 감소, 유지보수성 향상, 비즈니스 서비스 예외 번역, 가용성 향상, 비즈니스 계층에 더 간단하고 균일한 인터페이스를 제공, 성능 향상, 추가 레이어 도입

구현 방법

https://www.tutorialspoint.com/design_pattern/business_delegate_pattern.htm


Session Facade

비즈니스 계층 구성 요소를 캡슐화하고 거친 서비스를 원격 클라이언트에 노출합니다. 클라이언트는 비즈니스 구성 요소에 직접 액세스하는 대신 Session Facade에 액세스합니다.

 

Problem

비즈니스 구성 요소 및 서비스를 원격 클라이언트에 노출하려고 합니다.
Forces

 

  1. 클라이언트와의 긴밀한 결합을 방지하기 위해 클라이언트가 비즈니스 계층 구성 요소에 직접 액세스할 수 없도록 하고 싶습니다.
  2. Business Objects 및 기타 비즈니스 계층 구성 요소에 원격 액세스 계층을 제공하려고 합니다.
  3. 애플리케이션 서비스 및 기타 서비스를 집계하여 원격 클라이언트에 노출하려고 합니다.
  4. 원격 클라이언트에 노출되어야 하는 모든 비즈니스 논리를 중앙 집중화하고 집계하려고 합니다.
  5. 비즈니스 구성 요소와 서비스 간의 복잡한 상호 작용 및 상호 종속성을 숨겨서 관리 용이성을 높이고 논리를 중앙 집중화하고 유연성을 높이고 변경 사항에 대처하는 능력을 개선하려고 합니다.

Consequences

원격 클라이언트에 서비스를 제공하는 계층 도입, 계층 간의 결합 감소, 레이어링을 촉진하고 유연성과 유지보수성을 높입니다, 성능 향상, 세분화된 원격 방법 감소, 중앙 집중식 보안 관리, 트랜잭션 제어 중앙 집중화, 클라이언트에 더 적은 수의 원격 인터페이스 노출

구현 방법

https://www.tutorialspoint.com/design_pattern/facade_pattern.htm

 


Data Access Object

데이터 액세스 개체를 사용하여 영구 저장소에 대한 모든 액세스를 추상화하고 캡슐화합니다. 데이터 액세스 개체는 데이터를 가져오고 저장하기 위해 데이터 원본과의 연결을 관리합니다.

Problem

데이터 액세스 및 조작을 별도의 레이어에 캡슐화하려고 합니다.

Forces

  1. 영구 저장소의 데이터에 액세스하고 조작하기 위해 데이터 액세스 메커니즘을 구현하려고 합니다.
  2. 애플리케이션의 나머지 부분에서 영구 저장소 구현을 decouple하려고 합니다.
  3. RDBMS, LDAP, OODB, XML 리포지토리, 플랫 파일 등과 같은 다양한 유형의 데이터 소스에 대한 영구 메커니즘에 대한 균일한 데이터 액세스 API를 제공하려고 합니다.
  4. 데이터 액세스 논리를 구성하고 독점 기능을 캡슐화하여 유지 관리 및 이식성을 용이하게 하려고 합니다.

Consequences

중앙 집중식 제어, 투명한 사용성, 객체 지향 보기를 제공하고 데이터베이스 스키마를 캡슐화, 마이그레이션 용이, 모든 데이터 액세스 코드를 별도의 레이어로 구성

구현 방법

https://www.tutorialspoint.com/design_pattern/data_access_object_pattern.htm


Service Locator

서비스 로케이터를 사용하여 서비스 및 구성 요소 조회를 구현하고 캡슐화합니다. 서비스 로케이터는 조회 메커니즘의 구현 세부 정보를 숨기고 관련 종속성을 캡슐화합니다.

Problem

균일한 방식으로 비즈니스 구성 요소와 서비스를 투명하게 찾고자 합니다.

Forces

  1. JNDI API를 사용하여 엔터프라이즈 Bean 및 JMS 구성요소와 같은 비즈니스 구성요소와 데이터 소스와 같은 서비스를 조회하고 사용하려고 합니다.
  2. J2EE 애플리케이션 클라이언트에 대한 조회 메커니즘 구현을 중앙 집중화하고 재사용하려고 합니다.
  3. 레지스트리 구현에 대한 공급업체 종속성을 캡슐화하고 클라이언트로부터 종속성과 복잡성을 숨기려고 합니다.
  4. 초기 컨텍스트 생성 및 서비스 조회와 관련된 성능 오버헤드를 피하려고 합니다.
  5. 핸들 오브젝트를 사용하여 이전에 액세스한 엔터프라이즈 Bean 인스턴스에 대한 연결을 재설정하려고 합니다.

+ JNDI는 Java Naming and Directory Interface API의 머리글자입니다. 디렉터리 서비스에서 제공하는 데이터 및 객체를 발견(discover)하고 참고(lookup) 하기 위한 자바 API.

 

Consequences

추상화된 복잡성, 클라이언트에게 균일한 서비스 액세스 제공, EJB 비즈니스 구성 요소 추가 촉진, 네트워크 성능 향상, 캐싱을 통해 클라이언트 성능 향상

구현 방법

https://www.tutorialspoint.com/design_pattern/service_locator_pattern.htm


Transfer Object

전송 개체를 사용하여 계층 전체에 여러 데이터 요소를 전달합니다.

Problem

계층을 통해 여러 데이터 요소를 전송하려고 합니다.

Forces

  1. 클라이언트가 다른 계층의 구성 요소에 액세스하여 데이터를 검색하고 업데이트하도록 하려고 합니다.
  2. 네트워크를 통한 원격 요청을 줄이려고 합니다.
  3. 높은 네트워크 트래픽이 있는 응용 프로그램으로 인한 네트워크 성능 저하를 방지하려고 합니다.

Consequences

네트워크 트래픽 감소, 원격 개체 및 원격 인터페이스 단순화, 더 적은 수의 원격 호출로 더 많은 데이터 전송, 코드 중복 감소

 

구현 방법

 

제가 맡고있는 SpringBoot 프로젝트에선 JPA를 사용하고 있으며 분산 환경 데이터베이스를 구축하기 위해  AbstractRoutingDataSource 및 Spring Data JPA를 사용하여 Dynamic DataSource 라우팅을 하고 있습니다. 

@Data
@Configuration
@ConfigurationProperties(prefix = "test")
public class NamedDataSources {
   private List<NamedDataSource> namedDataSources;
}
@Getter
@Setter
public class NamedDataSource {
   private String name;
   private HikariConfig hikari;
}
  1. yml 파일에 입력된 db 설정 정보가 NamedDataSources 클래스에 로딩된다.
  2. DataSource를 생성한다. (DetermineRoutingDatasource)
    1. DetermineRoutingDataSource 인스턴스 생성
    2. DatermineRoutingDataSource에는 key,value 형태로 저장된 NamedDataSources 정보가 있다.
    3. Default 데이터소스가 등록된다.
    4. DataSoucre로 DetermineRoutingDataSource가 리턴된다.
  3. EntityManager Bean (LocalContainerEntityManagerFactory)을 등록한다.
    1. Datasource지정-> (2번에서 생성한 DetermineRoutingDataSource)로 설정
    2. Hibernate Property, Entity가 위치한 Package 지정
    3. Hibernate 기반으로 동작하는 것을 지정하는 JPA Vendor 설정
  4. TransactionManager Bean을 등록한다.
    1. LocalContainerEntityManagerFactory Bean을 주입받음
    2. Datasource와 EntityManagerFactoryBean에서 생성되는 EntityManagerFactory를 지정

 

@Configuration
@EnableJpaRepositories(basePackages = "com.xx.xxx.xx.xx",
        transactionManagerRef = "transcationManager",
        entityManagerFactoryRef = "entityManager")
@EnableTransactionManagement
public class NamedRoutingDataSources {

    private final TestProperties jpaProps;
    private final NamedDataSources namedDataSources;

    public NamedRoutingDataSources(TestProperties jpaProps, NamedDataSources namedDataSources) {
        this.jpaProps = jpaProps;
        this.namedDataSources = namedDataSources;
    }

    @Primary
    @Bean
    public DataSource createRoutingDataSource() {
        Map<Object, Object> targetDataSources = new HashMap<>();

        for (NamedDataSource namedDataSource : namedDataSources.getNamedDataSources()) {
            targetDataSources.put(DatabaseCluster.valueOf(namedDataSource.getName()), new HikariDataSource(namedDataSource.getHikari()));
        }

        DetermineRoutingDataSource routingDataSource = new DetermineRoutingDataSource();
        routingDataSource.setTargetDataSources(targetDataSources);
        routingDataSource.setDefaultTargetDataSource(targetDataSources.get(DatabaseCluster.MZ));

        return routingDataSource;
    }

    @Bean(name = "entityManager")
    public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean() {
        LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean();
        factoryBean.setDataSource(createRoutingDataSource());
        factoryBean.setPackagesToScan("com.xx.xxx.xx.xx");
        factoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
        factoryBean.setJpaProperties(initJpaHibernateProperties());
        return factoryBean;
    }

    private Properties initJpaHibernateProperties() {
        Properties properties = new Properties();
        properties.put(AvailableSettings.FORMAT_SQL, jpaProps.getProperties().getHibernate().isFormatSql());
        properties.put(AvailableSettings.SHOW_SQL, jpaProps.getProperties().getHibernate().isShowSql());
        return properties;
    }

    @Bean(name = "transcationManager")
    public JpaTransactionManager transactionManager(
            @Autowired @Qualifier("entityManager") LocalContainerEntityManagerFactoryBean entityManagerFactoryBean) {
        return new JpaTransactionManager(entityManagerFactoryBean.getObject());
    }
}

SpringBoot 프로젝트가 로딩 될 때 위와 같이 설정이 됩니다.

크게 보자면 먼저 Datasource를 생성하고, Spring 프로젝트에서 JPA를 사용하기 위해 EntityManager를 설정합니다. 그리고 Spring Container에서 동작하는 JPA의 기능을 활용하고, 스프링이 제공하는 일관성 있는 데이터 액세스 기술의 접근 방법을 적용할 수 있도록 LocalContainerEntityManager를 생성합니다.

 

 

AbstractRoutingDataSource는 조회 키를 기반으로 다양한 대상 데이터 소스 중 하나로 호출을 라우팅하는 DataSource의 추상 구현체입니다. AbstractRoutingDataSource는 현재 컨텍스트를 기반으로 실제 데이터 소스를 동적으로 결정하는 방법을 제공하기 위해 Spring 2.0.1 버전에 도입되었습니다. 컨텍스트 변경을 통해 전환되는 여러 데이터 소스의 맵을 유지 관리합니다.

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class DetermineRoutingDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return TestThreadLocal.getDatabaseCluster();
    }
}
import org.springframework.stereotype.Component;

@Component
public class TestThreadLocal {

    public static final ThreadLocal<DatabaseCluster> contextHolder = new ThreadLocal<>();

    public static DatabaseCluster getDatabaseCluster() {
        return contextHolder.get();
    }

    public static void setDatabaseCluster(DatabaseCluster databaseCluster) {
        contextHolder.set(databaseCluster);
    }

    public static void clear() {
        contextHolder.remove();
    }
}

https://www.websparrow.org/spring/spring-boot-dynamic-datasource-routing-using-abstractroutingdatasource

 

프로젝트에서 Hikari 옵션을 아래와 같이 정의하고 사용키로 하였다. 

        driver-class-name: org.postgresql.Driver
        auto-commit: true
        connection-init-sql: select 1
        minimum-idle: 3
        maximum-pool-size: 20
        connection-timeout: 600000
        validation-timeout: 120000
        idle-timeout: 108000000 # 600000*10*18 = 18hours
        max-lifetime: 0 # controlled by idle-timeout
        cachePrepStmts: true
        prepStmtCacheSize: 350
        prepStmtCacheSqlLimit: 2048
        useServerPrepStmts: true


**autoCommit:**
auto-commit설정 (default: true)
**connectionTimeout:**
pool에서 커넥션을 얻어오기전까지 기다리는 최대 시간, 허용가능한 wait time을 초과하면 SQLException을 던짐. 설정가능한 가장 작은 시간은 250ms (default: 30000 (30s))
**idleTimeout:**
pool에 일을 안하는 커넥션을 유지하는 시간. 이 옵션은 minimumIdle이 maximumPoolSize보다 작게 설정되어 있을 때만 설정.
pool에서 유지하는 최소 커넥션 수는 minimumIdle (A connection will never be retired as idle before this timeout.). 최솟값은 10000ms (default: 600000 (10minutes))
**maxLifetime:**
커넥션 풀에서 살아있을 수 있는 커넥션의 최대 수명시간. 사용중인 커넥션은 maxLifetime에 상관없이 제거되지않음. 사용중이지 않을 때만 제거됨. 풀 전체가아닌 커넥션 별로 적용이되는데 그 이유는 풀에서 대량으로 커넥션들이 제거되는 것을 방지하기 위함임. 강력하게 설정해야하는 설정 값으로 데이터베이스나 인프라의 적용된 connection time limit보다 작아야함. 0으로 설정하면 infinite lifetime이 적용됨(idleTimeout설정 값에 따라 적용 idleTimeout값이 설정되어 있을 경우 0으로 설정해도 무한 lifetime 적용 안됨). (default: 1800000 (30minutes))
**minimumIdle:**
아무런 일을 하지않아도 적어도 이 옵션에 설정 값 size로 커넥션들을 유지해주는 설정. 최적의 성능과 응답성을 요구한다면 이 값은 설정하지 않는게 좋음. default값을 보면 이해할 수있음. (default: same as maximumPoolSize)
**maximumPoolSize:** 
pool에 유지시킬 수 있는 최대 커넥션 수. pool의 커넥션 수가 옵션 값에 도달하게 되면 idle인 상태는 존재하지 않음.(default: 10)


PreparedStatement란:
데이터베이스 관리 시스템(DBMS)에서 동일하거나 비슷한 데이터베이스 문을 
높은 효율성으로 반복적으로 실행하기 위해 사용되는 기능을 말할다.


**cachePrepStmts (cachePreparedStatements)**
default: false, recommend: true
MySQL은 PreparedStatement Caching을 비활성화하고 있기 때문에, 이 옵션을 허용해줘야 아래의 옵션값들이 실제 DB에 영향을 줄 수 있다.

**prepStmtCacheSize (preparedStatementsCacheSize)**
default: 25, recommend: 250 ~ 500
MySQL 드라이버가 Connection마다 캐싱할 PreparedStatement의 개수를 지정하는 옵션이다. HikariCP에서는 250 ~ 500개 정도를 추천한다.

**prepStmtCacheSqlLimit (preparedStatementsCacheSqlLimit)**
default: 256, recommend: 2048
MySQL 드라이버가 캐싱할 PreparedStatement의 최대 길이를 지정하는 옵션이다. HikariCP 개발자들의 경험상, Hibernate와 같은 ORM framework를 사용하는 경우에 특히 이 기본값이 턱없이 모자란다고 한다.

**useServerPrepStmts (useServerSidePreparedStatements)**
default: false, recommend: true
Server-Side PreparedStatement를 사용하는 옵션이다. Server-Side Prepared



**PreparedStatement 동작방식**

1. 먼저 애플리케이션은 쿼리 틀을 만들고 이를 DBMS로 보낸다. 특정값은 지정하지 않은 채로 남겨진다 
      ex) INSERT INTO products (name, age) VALUES (?, ?);
2. 해당 쿼리를 컴파일하며(최적화 및 변환) 아직 실행하지 않고 결과만 저장한다.
3. 나중에 쿼리 틀의 변수에 값을 지정하면 DBMS는 (결과를 반환할 수도 있는) 문을 실행한다. 애플리케이션은 여러 값으로 원하는 횟수만큼 문을 실행할 수 있다. 
 
**Statement와 PreparedStatement차이**
Statement와 PreparedStatement의 아주 큰 차이는 바로 캐시(cache)사용여부이다.
Statement를 사용하면 매번 쿼리를 수행할 때마다 4단계를 거치게 되고(계속적으로 단계를 거치면서 수행)
PreparedStatement는 처음 한 번만 세 단계를 거친 후 캐시에 담아 재사용을 한다는 것이다. 

만약 동일한 쿼리를 반복적으로 수행한다면 PreparedStatment가 DB에 훨씬 적은 부하를 주며, 성능도 좋다.


출처
https://webstone.tistory.com/56
https://effectivesquid.tistory.com/entry/HikariCP-%EC%84%B8%ED%8C%85%EC%8B%9C-%EC%98%B5%EC%85%98-%EC%84%A4%EB%AA%85

모델 학습,데이터 결과 처리, 프로덕션 배포 및 SageMaker의 CI/CD에 대한 내용이다. 

  • SageMaker Training이란 무엇일까?
    • 완전 관리형 머신 러닝 학습 서비스
    • 데이터 과학자가 빠르고 쉽게 모델 개발 및 학습을 할 수 있도록 지원

 

두가지 타입으로 사용할 수 있다. 

고객이 준비할 건 파란 영역이며 AWS에서 제공하는 학습 컨테이너 이미지를 사용할 수도 있다. 추가로 필요한 패키지가 있는 경우는 Custom 학습 컨테이너 이미지를 직접 고객이 만들어서 올린 다음 사용할 수 있다.

 

작은 용량의 노트북 CPU를 띄워서 데이터 준비 및 코드를 작성하고, 모델 학습은 고성능 CPU에서 실행할 수 있게 한다.

 

모델 학습 환경의 구성

1. SageMaker 노트북 생성

2. 학습 코드 내 경로 수정

3. 학습 작업의 실행 노트북 작성

 

1. SageMaker 노트북 생성 

노트북 인스턴스에서 모델학습을 수행할 수 있지만, 학습의 확장성이 제한적입니다.

따라서, 별도 인스턴스를 띄워서 모델 학습을 진행하며, 이를 SageMaker Training이라고 합니다. 

비용을 절감, 학습의 확장성을 고려해서 노트북에서 학습하기 보다는 별도 인스턴스에서 학습하길 권장한다. 

학습이 끝나면 AWS에서 제공하는 학습 클러스터는 종료되고 과금되지 않는다. 

학습을 실행하게 되면 정의한 내용 대로 학습 인스턴스가 여러개 뜰 수도 있고 컨테이너 이미지가 여러개 뜰 수 있고 컨테이너들끼리 네트워크로 연결되어 분산 작업이 가능하다. 

 

동작방식은 어떻게 될까?

노트북에서 생성된 학습코드, S3에 저장된 데이터, AmazonECR에서 생성한 Custom학습 컨테이너 이미지가 학습 실행을 시작시키면 SageMaker 학습 클러스터에 복사되어 실행이 되고, 학습 종료 후 클러스터는 사라지게 된다. 

 

2. 학습 코드 내 경로 수정

 

학습코드를 노트북에 올리고, code 폴더는 자동으로 학습 클러스터의 학습 인스턴스에 자동으로 복사된다. 

경로가 너무 길면 별도 환경 변수를 활용하여 짧게 수정할 수도 있다.

 

클러스터가 종료되어도 산출물들은 저장되어야 하기 때문에 학습이 끝나면 위 경로에 자동으로 저장이 되게 한다.

3. 학습 코드 내 경로 수정

학습 클러스터의 인스턴스 종류/수 실행할 학습코드, 학습 환경 컨테이너 등을 Estimator로 정의

estimator에 정의하기
학습 클러스터에서 사용할 데이터 경로와 channel_name을 선언한 후 fit으로 실행하기
Estimator에 정의된 값에 더 알아보자
고가의 CPU, GPU를 활용해 빠르게 데이터를 가져오고 싶다 하면 Lustre를 활용하면 된다.
예상했던 것 보다 더 오래 동작하는 걸 방지하기 위해 최대 학습 수행시간을 조정한다.

작동원리

노트북에서 컨테이너 이미지를 받아와서 실행하여 디버깅을 한다. 비용 관점에서는 디버깅이 끝나면 노트북 인스턴스를 용량을 낮춰 비용을 절감한다.

 

간단하게 AWS 에서 제공하는 영상을 보면서 SageMaker가 어떤 건지 정도 알 수 있었다고 한다..

https://www.geeksforgeeks.org/static-synchronization-in-java/

위 글을 번역한 내용입니다.

 


 

동기화는 공유 리소스에 대한 멀티 스레드의 액세스를 컨트롤 할 수 있는 가능성입니다.

Java에서 동기화는 스레드 간의 안정적인 통신에 필수적이며, 이를 위해 Java에선 synchronized 키워드를 사용합니다.

 

Important Points Regarding Synchronization

  • Object level에 있는 메서드에만 해당됩니다.
  • 메서드 또는 블록이 동기화된 경우 object-level 잠금이 필요합니다.
  • 동기화는 deadlock 상태의 유일한 이유이기 때문에 Java에서 가장 위험한 단어입니다.
  • 필요할 땐 동기화된 키워드를 사용하고 동기화된 블록도 한번 사용해 보십시오.

Static Synchronization

동기화된 메서드는 순서가 지정된 출력을 가져오는 동작이 원하는대로 동작하지 않을 수 있습니다. 

클래스의 객체가 더 많을 경우 특정 인스턴스의 잠금만 획득합니다.

Synchronized을 유지하려면 정적 동기화로 달성할 수 있는 instance-level lock이 아닌  class-level lock이 필요합니다.

 

Static Synchronized 메소드는 두 개의 스레드가 synchronized 메소드에 대해 동시에 정적으로 작동할 수 없도록 Java에서 메소드를 동기화하는 메소드이기도 합니다. 유일한 차이점은 Static Synchronized를 사용하는 것입니다. 하나의 스레드만 메서드에서 작동하도록 class-level lock을 달성하고 있습니다. 스레드는 하나의 스레드만 정적 동기화 메서드에서 작동할 수 있도록 Java 클래스의 class level lock 을 획득합니다.

 

Syntax:

synchronized static return type class name{}

 

6개의 쓰레드가 있다고 가정하자. 실행 순서는 아래와 같다.

 

The complete declarations of  methods are:
method1: public static synchronized void method1()
method2: public static synchronized void method2()
method3: public static void method3()
method4: public synchronized int method4()
method5: public String method5()

 Threads and Classes

  1. 여기서 t1,t2… t6은 스레드 이름입니다.t1.method1()은 Manager 클래스의 클래스 수준 잠금을 획득하면서 실행을 시작합니다.
  2. t2.method2()는 실행 시작 시간을 기다립니다. 정적 동기화 메서드이므로 t1이 이미 클래스 수준 잠금을 획득했기 때문에 클래스 수준 잠금이 필요합니다. t2는 t1이 실행될 때까지 기다려야 합니다.
  3. t3.method2()는 클래스 수준 잠금이 필요하므로 대기하므로 t1이 잠금을 해제할 때까지 기다려야 합니다.
  4. t4.method3()은 잠금이 필요 없는 정적 메서드이므로 실행을 시작합니다.
  5. t5.method4()는 인스턴스 또는(일반) 수준의 동기화 메서드로 실행을 시작하고 개체 수준 잠금이 필요하므로 개체 수준 잠금을 얻습니다.
  6. t6.method5()는 인스턴스 메서드 또는 일반 메서드이므로 실행을 시작합니다.

 

Difference between Synchronized and Static Synchronized in Java

object-level lock. class-level lock.
메서드를 정적으로 선언할 필요는 없습니다.
해당 메서드는 정적으로 선언해야 합니다.
필요시 자주 사용됨 자주 사용 X
각 개체에 대해 다른 인스턴스가 생성됩니다.
전체 프로그램에 대해 단 하나의 인스턴스입니다.

+ Recent posts