전통적인 컴퓨터가 사용하는 비트(bit) 대신, 양자 컴퓨터는 양자 비트(큐비트, qubit)를 사용해 정보를 처리합니다.
큐비트는 0과 1 사이의 모든 상태를 동시에 가질 수 있습니다.
예를들면 3개의 고전 비트는 한 번에 하나의 상태(예: 000, 001, ... 등 8개 중 하나)를 표현하지만, 3개의 큐비트는 한 번에 모든 상태를 동시에 표현할 수 있습니다. 이를 통해 양자 컴퓨터는 병렬로 여러 계산을 동시에 수행할 수 있습니다. 큐비트가 늘어날수록 연산 능력이 지수적으로 증가합니다.
양자 컴퓨터는 확률적으로 정답을 찾아갑니다. 확률이 높다고 무조건 정답은 아니지만 이를 보완하기 위해 매우 많은 시도를 동시에 진행한다. (검산과정) → 간섭이 많을 수록 확률이 높다 (정답 가능성이 매우 높음)
📝Qubit(큐비트)
정보를 저장하고 처리하는 양자적 단위
📝양자 푸리에 변환(QFT)
양자 푸리에 변환의 경우 일상생활 문제를 해결하기 위한 양자역학의 시도입니다. 입력 데이터(시간 또는 공간에 따른 함수)를 주파수 기반의 성분으로 분해하는 과정을 의미합니다.
주기성 탐지, 최적화 문제 해결에 탁월하다 주 사용 분야는 양자 컴퓨팅이며 상태를 주파수 영역으로 변환시켜 사용한다. 주기가 있는 즉, 주기가 있다는 건 특징이 있다는 것이고 특징이 있는 곳에 활용하기 좋다는 말입니다.
📝쇼어 알고리즘
1994년 피터 쇼어(Peter Shor)가 제안한 양자 알고리즘으로, 큰 정수를 빠르게 소인수분해하는 방법입니다.
양자컴퓨터에서 중요한데 고전 컴퓨터에서는 소인수분해는 매우 시간이 많이 걸리는 작업인데 특히 큰 수는 소인수 분해가 사실상 불가능합니다. 하지만 해당 알고리즘은 이 문제를 효율적으로 해결할 수 있습니다.
RSA는 소인수 분해의 "어려움"을 기반으로 한 암호화 체계로 보안에 문제를 일으킬 수도 있다는 말이 있습니다.
📝그로버 알고리즘
양자 컴퓨팅에서 사용되는 검색 알고리즘으로, 미지의 데이터베이스에서 원하는 항목을 빠르게 찾는 문제를 해결합니다. 이는 고전적인 검색 알고리즘보다 제곱근 속도로 빠릅니다
고전역학의 경우 거시적인 곳에서 사용되며 위치와 속도를 알면 모든 상황은 예측이 가능하다.
양자역학
입자란 질량을 가지고 있어서 손으로 던질 수 있는 형태로 빛이 입자라는 게 판명나고 이거를 이중슬릿 실험으로 확인하려고 광자를 쐈을 때 파동의 성질인 간섭무늬가 나타났다.
빛이 입자가 아니라는 것인지 실험이 이상한 것인지 체크하기 위해 진짜 입자를 가지고 있는 전자를 발사했는데도 똑같은 현상이 나타났다. 이걸 확인하기 위해 이중 슬릿을 통과했을 때를 그 상황을 관측해봤다. 그러자 파동의 성질인 간섭이 아니라 입자의 성질이 되어 한곳에서만 관측 되었다.
관측을 한다는 건 빛이 반사되어서(빛의 충돌) 내 눈에 들어왔다는 것이다. 미시적인 곳에서는 입자와 파동의 성질을 동시에 가지게 되며 관측이 되었을 때 결정된다는 이론을 내게 된다. 이게 양자의 영역에서의 역학이다.
📝양자중첩, 양자얽힘, 양자붕괴
양자중첩
양자중첩은 입자가 동시에 여러 상태를 가질 수 있는 현상으로 측정이 되기 까지는 겹쳐진 상태로 존재합니다.
양자얽힘
특정 조건에서 두 개 이상의 입자가 서로 상태를 공유하며, 하나의 상태 변화가 즉각적으로 다른 입자에 영향을 미치는 양자적 현상입니다.
→ 두개의 종이가 있는데 적힌 숫자의 합이 10이 되는 종이일 때 매우 먼 거리에서 하나의 종이를 열면 하나의 종이에 숫자가 정해진다는 말 (물론 숫자가 적혀있는 건 관측이 된 상태이기 때문에 이미 정해진 거이지만 이해하려면 관측이 안 된상태로 적혀있다고 생각해야함)
양자중첩
양자붕괴는 중첩 상태가 특정 상태로 확정되는 과정을 의미합니다. 이는 관측에 의해 발생할 수 있습니다
📝슈뢰딩거의 고양이
슈뢰딩거는 양자역학 반대파로 그러면 고양이도 관측하기 전까지 죽어있는지 살아있는지 모르는 거 아니냐라는 비아냥으로부터 시작 되었는데 거시적인 관점에서도 양자역학이 적용된다고 믿기 때문에 일상생활에서 아주 이해하기 쉬운 예제라고 각광받게 된다.
📝코펜하겐 해석
코펜하겐 해석은 양자역학의 수학적 결과를 현실에서 어떻게 이해할 것인지에 대한 철학적 해석으로 슈뢰딩거 고양이 따위가 있습니다
📝파동함수
양자역학에서 입자의 상태를 수학적으로 기술하는 데 사용되는 함수입니다. 입자를 파동의 형태로 변환시켜서 설명할 때 사용 됩니다. (입자의 위치, 운동량, 에너지, 물리적 정보를 알려준다.)
데이터를 학습하여 특정 작업을 수행할 수 있도록 컴퓨터를 훈련시키는 기술입니다. 인간은 데이터를 넣어주고 학습방법에 따라 훈련시킵니다. 예를 들면 고양이 사진을 주고 고양이 특징을 알려줬을 때 다른 고양이 사진을 주면 고양이라고 판단합니다.
훈련기술은 아래와 같습니다.
지도학습
입력 데이터와 정답이 주어진 상태에서 학습 → 이메일 스팸 필터
비지도학습
정답이 없는 데이터를 분석하고 패턴을 학습 → 이미지 분류
강화학습
보상을 기반으로 최적의 행동을 학습 → 알파고(바둑)
📝딥러닝
머신러닝의 하위 분야로 인공 신경망을 기반으로 데이터를 학습시키는 기술입니다. 머신러닝과 달리 사람의 개입 없이 중요한 특징을 알아서 추출합니다. 예를 들면 고양이 사진을 여러개를 주면 알아서 특징을 파악하고 다른 고양이 사진을 줬을 때 얘가 고양인지 파악할 수 있습니다.
입력층
입력데이터를 신경망에 전달하는 첫번째 층이다. 데이터만 전달하며 함수나 가중치 계산은 없다.
은닉층
입력 데이터를 변환하고 학습하는 신경망의 중간층이다. 은닉층이 많아질수록 더 복잡한 패턴 학습이 가능하다. 일반적으로 그렇지만 많은 시도를 해서 자기가 원하는 최적의 결과를 얻을때까지 해야한다. (사실 나도 안 해봐서 잘 모름)
출력층
신경망의 최종 결과를 출력하는 층이다.
📝머신러닝 vs 딥러닝
특징
머신러닝
딥러닝
데이터 처리
사람이 데이터를 분석하고 특징을 수동 정의
신경망이 데이터를 입력받아 특징을 자동으로 추출
데이터 요구량
비교적 적은 데이터로도 학습 가능
방대한 데이터 필요 (방대할 수록 성능이 올라간다)
컴퓨팅 자원
적은 연산 자원으로도 학습 가능
고성능 GPU 필요
비정형 데이터 처리
주로 구조화된 데이터(엑셀, 테이블) 처리에 적합
이미지, 텍스트, 음성 등 비정형 데이터를 처리 가능
특징 추출
사람이 직접 특징을 추출하게끔 설계
자동으로 특징 추출
📝LLM (대규모 언어 모델)
대규모 데이터로 학습된 언어 모델로, 자연어를 이해하고 생성하는 데 사용됩니다. LLM은 특히 수십억에서 수조 개의 매개변수(parameters)를 가진 신경망 모델로, 복잡한 언어적 패턴을 학습할 수 있습니다.
📝생성형 AI
데이터를 학습하여 새로운 콘텐츠(텍스트, 이미지, 음악 등)를 생성할 수 있는 AI 기술입니다. LLM과 딥러닝 기술을 이용해 텍스트, 이미지, 음성 등을 생성합니다.
place-items, place-self의 경우는 grid안에서의 정렬이지만 flexbox처럼 전체에 대한 정렬의 경우는 좌우 정렬인 justify-content하고 상하 정렬인 align-content를 사용합니다. (place-content로 축약해서 상하좌우 정렬을 입력할 수 있습니다)
스코프란 스프링 컨테이너에서 빈을 관리하는 영역을 이야기한다. 기본적으로 스프링은 싱글톤 스코프를 가지고 이걸 가장 많이 사용한다. (이번 챕터는 그렇게 핵심은 아니지만 알아둬서 나쁠 거 없는 내용이고 언젠가는 쓰이는 내용이긴 하다) 참고로 이런 특별한 scope는 꼭 필요한 곳에만 최소화해서 사용하자, 무분별하게 사용하면 유지보수하기 어려워진다
📝프로토타입
싱글톤이 아니라 빈의 생성하고 의존관계 주입까지만 관여하고 더는 관리하지 않는 범위이다. 그렇기 때문에 직접 종료를 해줘야한다. 그렇지 않으면 엄청나게 메모리에 쌓일 것이다. 예를 들자면 다양한 클라이언트가 어떤 Bean을 요청했을 때 일반적인 싱글톤의 경우 해당 Bean을 공유해서 사용한다. (때문에 도중에 값을 수정하면 다른 사람에게도 영향이 감) 반면 프로토타입의 경우 요청마다 Bean을 새롭게 생성해서 독자적인 스코프 영역을 만든다.
// prototype 사용예제1
@Scope("prototype")
@Component
public class HelloBean {}
// prototype 사용예제2
@Scope("prototype")
@Bean
PrototypeBean HelloBean() {
return new HelloBean();
}
📝싱글톤에서 프로토타입 빈을 사용 주의점
싱글톤으로 생성된 Client Bean이라는 객체가 있고 의존관계 주입된 Bean이 프로토타입일 경우 ClientBean을 이용해 의존관계 주입된 Prototype으로 만들어진 Bean의 필드값을 바꾸는 경우를 생각해보자
일반적으로 Prototype이라서 서로 다른 클라이언트가 요청해도 필드값은 +1해도 각각 +1씩 가져야하지만 Client Bean이 넓은 범위 싱글톤이기 때문에 Prototype도 공유하게 된다.
📝 Prototype 유지하기 (Provider)
public class PrototypeProviderTest {
@Test
void providerTest() {
AnnotationConfigApplicationContext ac = new
AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);
ClientBean clientBean1 = ac.getBean(ClientBean.class);
int count1 = clientBean1.logic();
assertThat(count1).isEqualTo(1);
ClientBean clientBean2 = ac.getBean(ClientBean.class);
int count2 = clientBean2.logic();
assertThat(count2).isEqualTo(1);
}
static class ClientBean {
@Autowired
private ApplicationContext ac;
public int logic() {
PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
@Scope("prototype")
static class PrototypeBean {
private int count = 0;
public void addCount() {
count++;
}
public int getCount() {
return count;
}
@PostConstruct
public void init() {
System.out.println("PrototypeBean.init " + this);
}
@PreDestroy
public void destroy() {
System.out.println("PrototypeBean.destroy");
}
}
}
위와 같은 문제를 Provider라는 걸 이용해 해결이 가능하다. 일반적인 방법으로는 ClientBean에 있는 logic을 실행시킬때마다 Prototype이 자동으로 의존관계를 주입하게 하는게 아니라 내가 직접 스프링 컨테이너에서 찾아서 넣어주게끔 한다 ac.getBean(PrototypeBean.class); 이러한 과정을 DL(Dependency Lookup)이라고 한다. 위 코드는 DL을 내가 직접해주는게 스프링에서 제공해주는 게 있다.
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
스프링에서는 ObjectProvider는 빈을 컨테이너에서 대신 찾아주는 DL 서비스 제공해준다.
@Autowired
private Provider<PrototypeBean> provider;
public int logic() {
PrototypeBean prototypeBean = provider.get();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
또 다른 방법으로는 Java표준인 jakarta.inject.Provider을 사용하는 것이다. 얘는 따로 라이브러리 추가를 해주긴 해야한다.
📝 웹스코프
웹스코프란 웹에서 요청이 들어왔을 떄 언제까지 유지할지에 대한 범위이다.
request
HTTP Request와 동일한 생명주기에 요청마다 별도 빈 인스턴스가 생성되고 관리된다.
session
HTTP Session과 동일한 생명주기
application
서블릿 컨텍스트와 동일한 생명주기
websocket
웹 소켓과 동일한 생명주기
// Controller
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final MyLogger myLogger;
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) {
String requestURL = request.getRequestURL().toString();
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
}
// Logging Common
@Component
@Scope(value = "request")
public class MyLogger {
private String uuid;
private String requestURL;
public void setRequestURL(String requestURL) {
this.requestURL = requestURL;
}
public void log(String message) {
System.out.println("[" + uuid + "]" + "[" + requestURL + "] " + message);
}
@PostConstruct
public void init() {
uuid = UUID.randomUUID().toString();
System.out.println("[" + uuid + "] request scope bean create:" + this);
}
@PreDestroy
public void close() {
System.out.println("[" + uuid + "] request scope bean close:" + this);
}
}
// Service Area
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final MyLogger myLogger;
public void logic(String id) {
myLogger.log("service id = " + id);
}
}
위 예제는 여러사용자가 많이 방문하면 로깅이 많이 찍힐텐데 동시다발적인 경우 어떤 곳에서 에러가 발생했을 경우 어떤 흐름에서 끊겼는지 구분해줄 수 있다. (HTTP Request Bean을 독자적 관리하기 때문에 구분 가능) 위 코드를 사용하면 MyLogger를 의존관계 주입을 하려고했지만 Request Scope라서 Request요청이 들어와야 생성된다. 이럴 경우 MyLogger가 필요한 곳에서 넣지못해서 에러가 나게 된다.
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final ObjectProvider<MyLogger> myLoggerProvider;
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) {
String requestURL = request.getRequestURL().toString();
MyLogger myLogger = myLoggerProvider.getObject();
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
}
...
이러한 에러는 ObjectProvider로 해결이 가능하다 ObjectProvider는 스프링이 띄워질 때가 아니라 뒤늦게 요청이 들어와야 동적으로 만들어지거나 뒤늦게 조회가 필요한 경우 지연 조회 처리가 가능하다.
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
}
더 간단한 방법이 있는데 Proxy를 이용해 가짜 빈을 넣어줘서 있는 것처럼 동작하게끔한다. 그래서 에러가 나지 않는다.
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired // 생성자 1개일 때 생략 가능
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy
discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
생성자를 통해 의존관계를 주입 받습니다. 생성자가 딱 1개만 있으면 @Autowired를 생략해도 자동 주입이 됩니다. 여기에선 빈으로 생성된 MemberRepository, DiscountPolicy가 의존관계 주입되어 들어가게 됩니다.
수정자 주입
@Component
public class OrderServiceImpl implements OrderService {
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
@Autowired
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Autowired
public void setDiscountPolicy(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
}
setter를 통해 의존관계를 주입 받습니다. 여기에선 빈으로 생성된 MemberRepository, DiscountPolicy가 의존관계 주입되어 들어가게 됩니다.
필드 주입
@Component
public class OrderServiceImpl implements OrderService {
@Autowired
private MemberRepository memberRepository;
@Autowired
private DiscountPolicy discountPolicy;
}
필드에 바로 주입하는 방법입니다. 코드가 간별하지만 외부에서 변경이 불가능해서 테스트 하기 어렵다는 단점이 있다. 예를 들면 setDiscountPolicy, setMemberRepository같은 Setter가 존재하면 DI 컨테이너(스프링)를 따로 안 띄워도 직접 넣어서 테스트할 때 사용이 가능한데 필드만 있는 경우는 스프링을 다 띄우지 않는 이상 테스트하기 어렵다.
📝어떤 걸 사용해야할까?
생성자 주입을 사용해야합니다. 필드 주입의 경우 테스트가 어려우며 Setter의 경우 변경가능성이 있기 때문에 객체 생성시 1번만 호출되게 설계할 수 있습니다.(불변) 또한 순수 자바코드로 단위테스트도 가능합니다.
final 키워드 사용시 생성자에 값이 없으면 컴파일 오류를 발생시켜 장애를 방지할 수 있습니다.
DiscountPolicy를 상속받은 RateDiscountPolicy, FixDiscountPolicy가 있으면 위 코드와 같이 했을 때 필드명을 보고 주입하게 된다. 즉, RateDiscountPolicy를 주입하게 된다.
@Qualifier
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {}
@Autowired
public OrderServiceImpl(MemberRepository memberRepository,
@Qualifier("mainDiscountPolicy") DiscountPolicy
discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
Qualifier의 경우 @Qualifier에 등록한 Bean이름을 찾아서 주입합니다.
// 어노테이션 생성
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER,
ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {
}
@Component
@MainDiscountPolicy
public class RateDiscountPolicy implements DiscountPolicy {}
위 같이 @Qualifier지정할 때 따로 어노테이션을 따로 만들어서 사용할 수 있는데 이렇게 사용하면 컴파일시기에 에러를 잡아낼 수 있는 장점이 있다.
@Primary
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
public class FixDiscountPolicy implements DiscountPolicy {}
@Autowired
public OrderServiceImpl(MemberRepository memberRepository,
DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
Primary의 경우 여러개 빈이 있어도 Primary 등록한 최상위 것이 먼저 등록됩니다. Qualifier의 경우 코드가 지저분해지기 때문에 @Primray를 더 잘 사용합니다.
📝등록된 Bean 다 가져오기
public class AllBeanTest {
@Test
void findAllBean() {
ApplicationContext ac = new
AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);
DiscountService discountService = ac.getBean(DiscountService.class);
Member member = new Member(1L, "userA", Grade.VIP);
int discountPrice = discountService.discount(member, 10000,
"fixDiscountPolicy");
assertThat(discountService).isInstanceOf(DiscountService.class);
assertThat(discountPrice).isEqualTo(1000);
}
static class DiscountService {
private final Map<String, DiscountPolicy> policyMap;
private final List<DiscountPolicy> policies;
public DiscountService(Map<String, DiscountPolicy> policyMap,
List<DiscountPolicy> policies) {
this.policyMap = policyMap;
this.policies = policies;
System.out.println("policyMap = " + policyMap);
System.out.println("policies = " + policies);
}
public int discount(Member member, int price, String discountCode) {
DiscountPolicy discountPolicy = policyMap.get(discountCode);
System.out.println("discountCode = " + discountCode);
System.out.println("discountPolicy = " + discountPolicy);
return discountPolicy.discount(member, price);
}
}
}
DiscountPolicy 인터페이스를 상속받은 고정할인(fixDiscountPolicy)하고 퍼센티지할인(rateDiscountPolicy)이 Bean으로 만들어져있습니다. 여기에 사용자가 넘긴 정보에 따라 고정 또는 퍼센티지를 결정해야할 경우 DiscounyPolicy구현체인 Bean으로 만들어진 걸 가져와서 넘긴 파라미터에 따라 다르게 처리할 수 있습니다.
Map의 경우 키에 Bean의 이름이 들어가고 값에는 구현체가 들어가게 됩니다. List의 경우는 키값이 없기 때문에 구현체만 들어가게 됩니다.
📝 수동 등록 vs 자동 등록
수동등록
@Configuration
public class DiscountPolicyConfig {
@Bean
public DiscountPolicy rateDiscountPolicy() {
return new RateDiscountPolicy();
}
@Bean
public DiscountPolicy fixDiscountPolicy() {
return new FixDiscountPolicy();
}
}
Configuration을 쓰고 Bean을 그 안에서 직접 생성해서 관리합니다. 한 곳에 모여있기 때문에 보기가 편합니다.
자동등록
@Component
public class DiscountPolicy {
}
Component어노테이션 기반으로 만들어진 어노테이션을 붙이면 Component Scan범위에 있으면 찾아서 Bean 등록해줍니다
요즘은 자동등록을 많이 쓰지만 수동등록의 경우도 필요할 때 써야한다. 보통 업무로직(Controller, Service, Repository)의 경우 자동 등록을 사용하고 기술지원(로깅, AOP 등)의 경우 Configuration으로 수동관리하는 편이다.
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public class MyClass {
private final String name; // 생성자 매개변수로 포함됨
}
// 생성된 생성자:
// public MyClass(String name) {
// this.name = name;
// }
final로 된 필드기반으로 생성자를 생성합니다.
@Getter
import lombok.Getter;
@Getter
public class MyClass {
private String name;
private int age;
}
// 생성된 메서드:
// public String getName() { return name; }
// public int getAge() { return age; }
필드기반으로 Getter를 생성합니다.
@Setter
import lombok.Setter;
@Setter
public class MyClass {
private String name;
private int age;
}
// 생성된 메서드:
// public void setName(String name) { this.name = name; }
// public void setAge(int age) { this.age = age; }
필드기반으로 Setter를 생성합니다.
@ToString
import lombok.ToString;
@ToString
public class MyClass {
private String name;
private int age;
}
// 생성된 메서드:
// public String toString() { return "MyClass(name=" + this.name + ", age=" + this.age + ")"; }
필드기반으로 toString()메서드를 자동으로 생성합니다. 로깅시 객체안에 있는 값을 편하게 볼 수 있습니다.
@Data
import lombok.Data;
@Data
public class MyClass {
private String name;
private int age;
}
// 생성되는 메서드:
// - Getter/Setter for name and age
// - toString(), equals(), hashCode()