이야기를 나눠요
158만명의 커뮤니티!! 함께 토론해봐요.
인프런 TOP Writers
-
스프링 핵심 원리 - 고급편
JDK 동적 프록시 예제를 프록시 체이닝으로 구현...?
들어가기 전에김영한님의 스프링 핵심 원리 - 고급편 수업을 듣고 있던 중에 LogTraceBasicHandler에 필터링을 추가한다는 말을 듣고 이전 강의에서 말씀해주셨던 프록시 체이닝이 생각이 나서 "FilterHandler 이후에 LogTraceHandler 로 이어지는 프록시 체이닝을 보여주시려나" 보다 하고 있는데 그 둘을 합친 LogTraceFilterHandler을 생성하셔서 구현하시길래 "어라, JDK 동적 프록시는 프록시 체이닝으로 하기 까다로운가?" 라는 생각이 들었습니다.그렇다면 구현해보면 알 것 같았기 때문에, 한 번 저의 식으로 구현을 해보았습니다. 그리고 아래의 내용은 스프링 AOP와 CGLIB의 진도를 나가기 전에 작성되었습니다.목표강의 예제의 구조는 다음과 같습니다./** * JDK 동적 프록시 사용<br> * - {@link InvocationHandler} JDK 동적 프록시에 로직을 적용하기 위한 Handler<br> * - {@link PatternMatchUtils#simpleMatch}로 WhiteList 기반 URL 패턴 필터링 */ @Slf4j @RequiredArgsConstructor public class LogTraceFilterHandler implements InvocationHandler { private final Object target; private final LogTrace logTrace; private final String[] patterns; @Override public Object invoke( Object proxy, Method method, Object[] args ) throws Throwable { // patterns 에 해당 메서드 이름이 없다면, 바로 목표로 이동 if (!PatternMatchUtils.simpleMatch(patterns, method.getName())) { return method.invoke(target, args); } // LogTrace 로직 실행 TraceStatus status = null; try { String message = method.getDeclaringClass().getSimpleName() + "." + method.getName() + "()"; status = logTrace.begin(message); Object result = method.invoke(target, args); logTrace.end(status); return result; } catch (Exception e) { logTrace.exception(status, e); throw e; } } } 보면, 필터를 수행하는 로직과 로깅을 수행하는 로직이 하나의 메서드로 합쳐져 있습니다. 물론, 저 두 개의 로직을 메서드로 따로 빼면 될 일이긴 합니다만… 저의 SRP 영혼이 슬프게 울고 있더군요.그리고 이전 수업 내용에 프록시의 장점 중 프록시 체이닝이란 것도 있기도 했고, Spring의 Filter도 이런식으로 구현되어 있을거니 나눠봤습니다.제가 구현하고자하는 내용은 아래와 같습니다.흐름만 봐서는 그냥 위의 코드와 동일하지만, 중요한 점은 필터를 담당하는 객체와 로깅을 담당하는 객체가 분리되었다는 것입니다. 그럼 구현을 한 번 해보죠.구현FilterHandler/** * 타겟의 메서드 이름을 필터링하는 Handler<br> * {@link PatternMatchUtils#simpleMatch}를 이용하여 패턴 검증<br> * - 해당 메서드의 이름이 패턴과 일치한다면: {@link #nextHandler}<br> * - 해당 메서드의 이름이 패턴과 일치하지 않는다면: {@link #target} * * @author MinyShrimp * @see Proxy * @see InvocationHandler * @see PatternMatchUtils#simpleMatch(String[], String) * @since 2023-03-02 */ public class FilterHandler implements InvocationHandler { private final Object target; private final Object nextHandler; private final String[] methodPatterns; /** * @param target 최종 목표 구현체 * @param nextHandler 다음 ProxyHandler * @param methodPatterns 필터링을 원하는 패턴 목록 - {@link PatternMatchUtils#simpleMatch} */ public FilterHandler( Object target, Object nextHandler, String[] methodPatterns ) { this.target = target; this.nextHandler = nextHandler; this.methodPatterns = methodPatterns; } @Override public Object invoke( Object proxy, Method method, Object[] args ) throws Throwable { Object next = PatternMatchUtils.simpleMatch(methodPatterns, method.getName()) ? nextHandler : target; return method.invoke(next, args); } } 필터를 담당하는 프록시 핸들러입니다. 이 핸들러는 단순히 입력받은 패턴을 조사해 맞으면 다음 핸들러로, 맞지 않으면 바로 목표 구현체로 이동되도록 구현되었습니다.생성자를 보시면 알 수 있겠지만, 최종 목표 구현체(여기서는 OrderControllerV1Impl), 다음 프록시 핸들러(여기서는 LogTraceHandler), 그리고 패턴 패칭을 원하는 문자열 배열을 받습니다.이 예제와는 상관없지만, 개인적으로는 편의를 위해 생성자를 하나 더 만들어서 methodPattern이 배열이 아닌 하나의 문자열만 받을 수 있도록 구현해도 괜찮다고 생각이 듭니다. 하지만, 당장은 사용하지 않기 때문에 제거를 했습니다.그리고 final 맴버변수를 받기 위해 @RequiredArgsConstructor를 사용해도 괜찮지만, 그렇게 하게되면 위와 같이 주석을 남길 수 없기 때문에 사용하지 않았습니다.LogTraceHandler/** * Logging Handler<br> * {@link LogTrace}를 이용하여 로그 출력 * * @author MinyShrimp * @see Proxy * @see InvocationHandler * @see LogTrace * @see ThreadLocalLogTrace * @since 2023-03-02 */ public class LogTraceHandler implements InvocationHandler { private final Object target; private final LogTrace logTrace; /** * @param target 목표 구현체, 다음 ProxyHandler * @param logTrace {@link LogTrace} 구현체 */ public LogTraceHandler( Object target, LogTrace logTrace ) { this.target = target; this.logTrace = logTrace; } @Override public Object invoke( Object proxy, Method method, Object[] args ) throws Throwable { TraceStatus status = null; try { String message = method.getDeclaringClass().getSimpleName() + "." + method.getName() + "()"; status = logTrace.begin(message); Object result = method.invoke(target, args); logTrace.end(status); return result; } catch (Exception e) { logTrace.exception(status, e); throw e; } } } 로깅을 담당하는 프록시 핸들러입니다. 이 핸들러는 기존 강의에서 사용된 LogTrace를 받아 로그 메시지를 출력하는 역할을 수행합니다. 강의에서 제작한 LogTraceBasicHandler와 동일하기 때문에 설명을 생략합니다.다만, 정말 개인적인 아쉬움이긴 합니다만, 위의 TraceStatus를 받아옴에 있어서 단순히 저 Exception 하나 때문에 status 변수를 try 외부에 null로 선언하고 재할당 해주는 부분이 너무 아쉬웠습니다. 이를 해결하기 위해선 몇가지 방법이 있긴 합니다만, 이전 시간에 배운 ThreadLocal로 한 번 바꿔보겠습니다. ( 단순히 멤버 변수로 할당하면 동시성 문제가 발생합니다. )public class LogTraceHandler implements InvocationHandler { // 동시성 문제를 해결하기 위해 ThreadLocal 사용 private final ThreadLocal<TraceStatus> thStatus = new ThreadLocal<>(); // 중간 부분 생략 @Override public Object invoke( Object proxy, Method method, Object[] args ) throws Throwable { try { String message = method.getDeclaringClass().getSimpleName() + "." + method.getName() + "()"; TraceStatus status = logTrace.begin(message); // 여기서 생성 thStatus.set(status); // ThreadLocal에 저장 Object result = method.invoke(target, args); logTrace.end(status); return result; } catch (Exception e) { TraceStatus status = thStatus.get(); // ThreadLocal에서 값을 가져옴 logTrace.exception(status, e); throw e; } finally { if (logTrace.isFirstLevel()) { thStatus.remove(); // 사용을 마치면 제거하자. } } } } 재할당하는 부분이 사라졌습니다! 이제 status를 받아오기 위해 ThreadLocal.get()을 사용하면 됩니다.그런데 여기서 주의사항이 있습니다. ThreadLocal의 사용이 끝나면 반드시 remove를 통해 지워줘야 합니다. 그래서 위와 같이 finally를 이용해 TraceId의 Level이 0인지 확인하고 0이면 remove를 하도록 작성해보았습니다. TraceId는 LogTrace가 가지고 있으니 넘겨주면 되겠군요.public class ThreadLocalLogTrace implements LogTrace { // 중간 생략 // LogTrace 인터페이스에도 추가해줍니다. @Override public boolean isFirstLevel() { return traceIdHolder.get().isFirstLevel(); // 그대로 넘겨줍니다. } } 좋습니다. 해결이 된 것 같군요. …과연 그럴까요? 이것 또한 버그가 있습니다.TraceId 는 ThreadLocalLogTrace에서 ThreadLocal로 잡고 있는 값입니다. 이것 또한 우리는 이전 시간에서 remove를 해주었습니다.public class ThreadLocalLogTrace implements LogTrace { // 중간 생략 /** * 이전 TraceID 로 전환<br> * - {@link #complete}에서 호출 */ private void releaseTraceId() { TraceId traceId = traceIdHolder.get(); if (traceId.isFirstLevel()) { traceIdHolder.remove(); // TraceId의 ThreadLocal을 제거한다. } else { traceIdHolder.set(traceId.createPreviousId()); } } } 겉보기에는 문제가 없어보입니다. 맞습니다. 평소에는 문제가 없습니다. 그런데 FirstLevel이 0이 되었을 때 문제가 발생합니다. 찬찬히 살펴보죠.위의 코드에서는 TraceStatus를 제거할 때는 finally에서 진행되고, TraceId를 제거할 때는 releaseTraceId에서 제거됩니다. 그리고 이 releaseTraceId는 end()와 exception()메서드에서 실행됩니다!즉, TraceStatus를 제거하기 위해 isFirstLevel() 메서드에서 traceIdHolder.get()을 사용하면, null을 리턴합니다. 그리고, null.isFirstLevel()은 NPE를 발생시킵니다. 그래서 아래와 같이 수정되어야 합니다.@Override public boolean isFirstLevel() { // releaseTraceId에서 제거되었다면 TraceId의 Level도 0이라는 소리. return traceIdHolder.get() == null; } DynamicProxyConfig/** * JDK 동적 {@link Proxy}를 스프링 빈으로 등록하기 위한 설정 파일 * * @author MinyShrimp * @see Proxy * @see FilterHandler * @see LogTraceHandler * @since 2023-03-02 */ @Configuration public class DynamicProxyConfig { /** * {@link FilterHandler}에서 사용하는 필터링 조건들, Whitelist 방식. */ private static final String[] METHOD_PATTERNS = { "request*", "order*", "save*" }; /** * @param target 최종 목표 구현체, 예) {@link OrderControllerV1Impl} * @param logTrace {@link LogTrace} * @return {@link FilterHandler} -> {@link LogTraceHandler} -> {@link OrderControllerV1Impl} */ private static Object filterLogProxyFactory( Object target, LogTrace logTrace ) { // 타겟이 상속받은 인터페이스들 중 첫 번째를 가져온다. // 해당 예제의 목표 타겟인 app.v1 들의 구현체들은 모두 인터페이스를 하나만 가지고 있기 때문에 가능하다. Class<?> superIntf = target.getClass().getInterfaces()[0]; // LogTraceProxy 생성 Object logTraceProxy = Proxy.newProxyInstance( superIntf.getClassLoader(), new Class[]{superIntf}, new LogTraceHandler(target, logTrace) ); // LogTraceProxy, 목표 타겟을 담은 FilterProxy 생성 return Proxy.newProxyInstance( superIntf.getClassLoader(), new Class[]{superIntf}, new FilterHandler(target, logTraceProxy, METHOD_PATTERNS) ); } /** * @return {@link OrderControllerV1Impl}의 Proxy */ @Bean OrderControllerV1 orderControllerV1(LogTrace logTrace) { OrderControllerV1Impl target = new OrderControllerV1Impl(orderServiceV1(logTrace)); return (OrderControllerV1) filterLogProxyFactory(target, logTrace); } /** * @return {@link OrderServiceV1Impl}의 Proxy */ @Bean OrderServiceV1 orderServiceV1(LogTrace logTrace) { OrderServiceV1 target = new OrderServiceV1Impl(orderRepositoryV1(logTrace)); return (OrderServiceV1) filterLogProxyFactory(target, logTrace); } /** * @return {@link OrderRepositoryV1Impl}의 Proxy */ @Bean OrderRepositoryV1 orderRepositoryV1(LogTrace logTrace) { OrderRepositoryV1Impl target = new OrderRepositoryV1Impl(); return (OrderRepositoryV1) filterLogProxyFactory(target, logTrace); } } 위에서 제작한 FilterHandler와 LogTraceHandler를 스프링 빈으로 등록하기 위한 설정 클래스입니다. 기본 베이스는 기존 강의에서 제작한 DynamicProxyFilterConfig와 동일합니다. 차이점이 있다면 각 빈에서 프록시 핸들러를 반환할때 발생하던 중복 코드를 filterLogProxyFactory() 메서드로 합쳐준 것 뿐입니다.그렇기 때문에 가장 중요한 filterLogProxyFactory()에 대해 설명하겠습니다./** * 이 메서드를 사용하기 위해선 FilterHandler와 LogTraceHandler는 * target의 첫 번째 인터페이스를 기반으로 프록시를 생성할 수 있어야 한다. * * @param target 1. 최종 목표 구현체, 예) {@link OrderControllerV1Impl} * @param logTrace {@link LogTrace} * @return {@link FilterHandler} -> {@link LogTraceHandler} -> {@link OrderControllerV1Impl} */ private static Object filterLogProxyFactory( Object target, LogTrace logTrace ) { // 2. 타겟이 상속받은 인터페이스들을 가져온다. Class<?>[] supIntfs = target.getClass().getInterfaces(); // 3. LogTraceProxy 생성 Object logTraceProxy = Proxy.newProxyInstance( supIntfs[0].getClassLoader(), supIntfs, new LogTraceHandler(target, logTrace) ); // 4. LogTraceProxy, 목표 타겟을 담은 FilterProxy 생성 return Proxy.newProxyInstance( supIntfs[0].getClassLoader(), supIntfs, new FilterHandler(target, logTraceProxy, METHOD_PATTERNS) ); } 이 메서드의 과정은 다음과 같습니다.타겟 객체(OrderControllerV1Impl)를 파라미터로 받아온다.타겟이 상속받은 인터페이스들의 정보(supIntfs)를 가져온다.그 정보를 이용해 LogTraceHandler의 프록시(logTraceProxy)를 생성한다.FilterHandler의 프록시를 생성하고 반환한다.기존 코드와 다른 점은 newProxyInstance()를 사용할 때, ClassLoader를 타겟의 첫 번째 인터페이스로 가져오는 것과, 두 번째 인자에 타겟의 모든 인터페이스들을 넣어 주는 부분입니다.이렇게 한 이유는 다름이 아니라, 이 메서드를 사용하는 모든 타겟 객체가 하나의 인터페이스만 상속받으며, 그 인터페이스를 이용해 핸들러들을 프록시로 만들어도 문제가 없기 때문입니다. 만약, 여러 개의 인터페이스를 상속받으며, 첫 번째 인터페이스를 기반으로 프록시를 생성하면 안되는 경우에는 위의 메서드를 사용할 수 없습니다. 그 때는 파라미터를 하나 추가해서 ClassLoader를 받아오면 됩니다.추가로 주의할 점은 FilterHandler 생성자에 LogTraceHandler를 주입한 것이 아닌, LogTraceProxy를 주입했다는 점입니다. LogTraceHandler도 invoke 함수가 제공되지만, 이는 Method의 invoke 함수와 무관하기 때문에 작동되지 않습니다. (예외가 발생합니다.)참고로, MainApplication에 해당 설정 파일을 등록하는 부분은 생략했습니다.완성…?이 구조를 완성했습니다. 구조 자체는 간단합니다만, 이쁘게 만들려다보니 신경써야할 부분이 좀 많았습니다.물론, 처음에 코드를 작성할 때 TraceStatus를 멤버 변수로 등록했다가 동시성 문제도 터지고, 위에서 설명한 finally에서도 NPE가 발생하기도 하고, FilterHandler에 LogTraceProxy를 넣어야하는데 LogTraceHandler를 넣어서 체이닝이 안되기도 했습니다만 구현해 놓고 보니 뿌듯합니다.하지만 아직 미심쩍은 부분과 아쉬운 부분이 보입니다.“ThreadLocal가 좋은건 알겠는데 이렇게 많이 사용해도 괜찮나…?”ThreadLocal의 가장 큰 주의점은 쓰레드 풀 환경에서 remove를 해주지 않는다면 다른 유저가 그 정보를 볼 수 있다는 점입니다. 또한, ThreadLocalMap은 크기가 커지면 커질 수록 2배의 크기로 할당됩니다. 지금까지는 별 문제가 없어보이지만, 다른 프로그래머가 코드를 수정하여 remove가 실행이 안되게 되거나, 비즈니스 로직 변경으로 인해 급하게 수정하다가 remove를 놓치게 되면 위의 문제는 꽤 심각하게 발생합니다. 이때는 어떻게 대처를 해야하나요? 아니면 위 문제는 별로 신경쓰지 않아도 되나요?“지금처럼 핏한 상황은 잘 작동하지만, 이 코드를 확장하려면 고칠 부분이 많네…”꽤 만족할만한 코드 퀄리티라고 생각은 합니다만, 위의 코드를 재사용하여 확장해야 하는 상황이 온다면 고쳐야할 부분이 눈에 들어옵니다. 일례로, 목표 타겟의 인터페이스 상속이 늘어나고 순서가 바뀌면 위의 코드의 filterLogProxyFactory는 더이상 사용할 수 없습니다.그리고 Handler가 늘어나면 설정 파일에서 그에 맞게 주입을 먼저 해주어야합니다. 이는 전략 패턴의 단점과도 연결됩니다. 또한, FilterHandler 와 같이 분기점에 따른 Proxy 변경도 많아지게 되면 일반화를 진행해야합니다.(BranchHandlerFactory 와 비슷한 이름으로..)정리사실, 위의 로직들은 JDK 동적 프록시가 아닌 다른 방법으로 구현하는게 맞습니다. 스프링 MVC에서 배운 필터와 인터셉터도 있고, (저는 아직 진도를 안 나갔습니다 만은)앞으로 배울 스프링 AOP가 해결 방법이 될 수도 있습니다. 그럼에도 이렇게 시간을 들여 글을 쓰는 이유는 다음과 같습니다.코드를 구현할 때 어떤 방식으로 구현 하는지 생각을 정리하고 기록을 남기기 위함 이었습니다.혼자서 코드를 작성하는 것은 언제나 자신과의 싸움을 하고 있다는 말과 같습니다. 보통 이런 상황에서 누군가에게 피드백을 받기란 요원한게 사실입니다. 그리고 그 기간이 길어지면 길어질수록 현재 자신의 위치가 어느 정도인지 짐작조차 할 수 없게 되고 다른 사람에게 물어볼때 어떤 방법으로 물어봐야하는지 모를 수 밖에 없습니다.“지금 짜고 있는 코드가 좋은 코드인가?” 과연 어떤 프로그래머가 이 생각을 하지 않겠냐 만은, 혼자서 코드를 작성하면 정말 나쁜 코드를 작성 하더라도 위에 대한 판단을 내릴 수가 없습니다. 또한, 나쁜 코드를 벗어나서 좋은 코드로 향하는 방법을 알고 싶어도 키워드를 모르니 방법이 없는 거지요.그래서 저의 코드 구현 방식을 공유하고 다른 분들에게 피드백을 받기 위해 이 글을 작성했습니다.현재 구현한 이 방법보다 더 좋은 방법이 무엇인지, 어떤 사이트 이펙트가 발생하는지, 내가 생각하고 있는 개념이 맞는지, JavaDoc 쓰는 방법은 올바른지, 등등 알고 싶은게 많습니다. (그러니까 비-법 소스 주세요!!!)꼭 긴글이 아니더라도 지나가는 말처럼 짧은 키워드만 툭툭 던져주셔도 저같이 공부하시는 분들에게는 많은 도움이 됩니다. 긴 글 읽어주셔서 감사드리며, 저와 같은 다른 취준생 여러분들도 다 같이 화이팅입니다. ^^7
-
10주완성 C++ 코딩테스트 | 알고리즘 코딩테스트
게임회사 코테 준비
안녕하세요.게임회사를 준비중인데 게임회사 코테도 이 강의만으로 커버가 가능할까요?아니면 이 강의에서 다루지 않은게 있다면 무엇이 있을까요?
-
나도코딩의 자바 기본편 - 풀코스 (20시간)
강사님, 혹시 자바 관련해서 다른 내용을 추가로 다루실 예정이 있으실까요?
보통 자바 강의를 들으시는 분들은 대다수 다음 테크로 스프링이나 안드로이드로 넘어가려는 목적을 가지신 분들이 많을 것 같은데요! 보아하니 대체적으로 스프링 강의에서는 Optional 같은 문법들을 많이 사용하는 것 같더라구요. 해서 혹시 다음 기획하시는 강의나 아니면 현재 강의에서 추가적인 내용을 더 다루실 계획이 있으신지 궁금하여 문의드립니다!
-
따라하며 배우는 노드, 리액트 시리즈 - 레딧 사이트 만들기(NextJS)(Pages Router)
context-api쪽 auth.tsx를 제 나름대로 리팩토리 해보았습니다.
[auth.tsx]import { User } from '@/shared/interfaces/user.interface'; import { createContext, FC, PropsWithChildren, useContext, useReducer, Dispatch, ReactNode, } from 'react'; interface AuthState { authenticated: boolean; user: User | undefined; loading: boolean; } export type AuthAction = | { type: 'LOGIN'; payload: User } | { type: 'LOGOUT' } | { type: 'STOP_LOADING' } | { type: undefined }; const initialState: AuthState = { authenticated: false, user: undefined, loading: true, }; const AuthContext = createContext<{ state: AuthState; dispatch: Dispatch<AuthAction>; }>({ state: initialState, dispatch: () => null, }); const authReducer = (state: AuthState, action: AuthAction): AuthState => { switch (action.type) { case 'LOGIN': return { ...state, authenticated: true, user: action.payload, }; case 'LOGOUT': return { ...state, authenticated: false, user: undefined, }; case 'STOP_LOADING': return { ...state, loading: false, }; default: throw new Error(`Unknown action type: ${action.type}`); } }; const AuthProvider: FC<PropsWithChildren<{ children?: ReactNode }>> = ({ children, }) => { const [state, dispatch] = useReducer(authReducer, initialState); return ( <AuthContext.Provider value={{ state, dispatch }}> {children} </AuthContext.Provider> ); }; const useAuthStateDispatch = () => useContext(AuthContext); export { AuthProvider, useAuthStateDispatch }; [login.tsx]import InputGroup from '@/components/ui/field/InputGroup'; import axios from 'axios'; import { AuthAction, useAuthStateDispatch } from 'context/auth'; import { NextPage } from 'next'; import Link from 'next/link'; import { useRouter } from 'next/router'; import React, { FormEvent, useState } from 'react'; const LoginPage: NextPage = () => { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [errors, setErros] = useState<any>({}); const router = useRouter(); const { dispatch } = useAuthStateDispatch(); const handleSubmit = async (e: FormEvent) => { e.preventDefault(); try { const res = await axios.post( '/auth/login', { password, username }, { withCredentials: true }, ); dispatch({ type: 'LOGIN', payload: res.data?.user }); router.push('/'); } catch (error: any) { console.log(error); setErros(error.response.data || {}); } }; return ( <div className="bg-white"> <div className="flex flex-col items-center justify-center h-screen p-6"> <div className="w-10/12 mx-auto md:w-96"> <h1 className="mb-2 text-lg font-medium">로그인</h1> <form onSubmit={handleSubmit}> <InputGroup placeholder="Username" value={username} setValue={setUsername} error={errors.username} /> <InputGroup placeholder="Password" value={password} setValue={setPassword} error={errors.password} /> <button className="w-full py-2 mb-1 text-xs font-bold text-white uppercase bg-gray-400 border border-gray-400 rounded"> 로그인 </button> </form> <small> 아직 아이디가 없나요? <Link href="/register" className="ml-1 text-blue-500 uppercase"> 회원가입 </Link> </small> </div> </div> </div> ); }; export default LoginPage; 누군가에겐 도움이 되지 않을까해서 남겨 보아요!
-
[중급편] 코인 가격 모니터링 앱 제작 (Android Kotlin)
currentpricelist 에서 data의 자료형을 map으로 쓰는 이유가 궁금합니다.
data class CurrentPriceList ( //데이터 타입을 가공하기 위한 작업이다. val status : String, //가공하기 위해 map을 쓴다. val data : Map<String, Any> ) 이런 코드가 있는데, data부분을 map으로 쓴 구체적인 이유가 궁금합니다. 또한, 각각의 키 부분에는 어떤 값이 저장되는지도 궁금합니다. 감사합니다!
-
[중급편] 코인 가격 모니터링 앱 제작 (Android Kotlin)
빗썸 api로 체결 내역을 파싱하는 과정에서 나는 오류를 알고싶습니다.
try { val gson = Gson() val gsonToJson = gson.toJson(result.data) val gsonFromJson = gson.fromJson(gsonToJson, RecentPriceData::class.java) //listof을 통해 list를 생성하고, gsonfromjson을 감싼다. val tradeHistoryList1 = TradeHistoryResult(coin, listOf(gsonFromJson) ) //우리가 정의한 리스트에 값을 추가한다. tradeHistoryList.add(tradeHistoryList1) Timber.d("체결내역 불러오기 성공") } catch (e: java.lang.Exception) { Timber.d("체결내역을 가져오는데 오류가 발생")개복치님, 안녕하세요? 강의를 잘 듣고 저만의 프로젝트를 개발하고 있습니다.빗썸 api를 통해 거래내역을 가져오는 기능을 구현하려 하는데,위 코드에서 오류가 발생하고 계속catch구문이 실행됩니다. 아래는 자세한 오류 구문입니다.D/SelectViewModel$getCurrentCoinList: com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 1 column 2 path $ 제가 api사이트를 들어가본 결과,(예:https://api.bithumb.com/public/transaction_history/BTC_KRW)date 부분이 []로 시작하는 리스트인데 이를 gson json으로 파싱하는 부분에 대해 잘 모르겠습니다.혹시 시간이 나신다면 이 문제를 해결해주신다면 대단히 감사드리겠습니다. 다음 수업도 기대하고 있습니다. 감사합니다! 제 깃허브에 프로젝트가 있습니다. https://github.com/guraudrk/coco/tree/master/app
-
개발자가 되기 위한 공부는?
목표 제가 공부하고싶은 부분은 짧게는 관련 업계로의 취업을 위한 공부 최종적으로 정말 원하는 것은 앱개발, 게임개발, JAVA관련(플래시?부분) 공부를 하고 싶습니다. 현재 상황 생활코딩과 인프런 무료강의 등을 활용하여 기초를 다지고 있습니다(주로 생활코딩) 1. 독학의 속도가 느리다고 생각이 들고 2. 국비지원을 통한 교육을 받을 예정인데 어느 방향으로 어떻게 공부해야할 지 모르겠습니다 (국비교육 어떤 분야로 신청해야할 지) 2번에 대한 답변을 주실 수 있을까요..?ㅠㅠ
-
제2회 ETRI 휴먼이해 인공지능 논문경진대회
○ 접수방법- 대회 안내 및 참가신청서 링크: https://aifactory.space/competition/detail/2234 ○ 논문 모집분야 및 주제- 반드시 아래 기재된 분야별로 할당된 ETRI 나눔 데이터셋과 주제를 활용한 연구이어야 합니다- 분야1: 라이프로그 데이터셋 활용 인식 및 추론 기술 분야- 분야2: 멀티모달 감정 데이터셋 활용 감정 인식 기술 분야- 각 분야에 대한 활용데이터 및 논문 주제는 대회 안내 태스크 페이지를 참고하시길 바랍니다○ 주최/후원/운영- 주최: 한국전자통신연구원 (ETRI)- 후원: 과학정보기술통신부, 국가과학기술연구회 (NST)- 운영: 인공지능팩토리 (AIFactory)○ 참가자격- 일반 성인(만 19세 이상의, 중/고등학교에 재학중이 아닌 자) 누구나 참가 가능- 1인 팀으로도 참가 가능하며 팀 구성 시 인원 제한 없음- 단, 지도교수는 논문저자에 명시하고 팀에서는 제외○ 기간 및 일정- 참가자 접수기간: 2월 17일 (금) ~ 4월 14일 (금) 8시- 온라인 사전 설명회: 3월 15일 (수) 17시 *온라인 접속 링크는 추후 본 페이지에 공지됩니다- 1차 논문 접수 마감일: 4월 14일 (금) 23시 59분- 1차 합격자 공지: 5월 17일 (수)- 2차 발표평가 및 시상식: 6/19 (월) *제주에서 진행되며 상세 사항은 대상자에 한하여 안내드립니다※ 본 대회는 2023 한국컴퓨터종합학술대회(KCC2023)와 연계하여 진행되며, 원활한 대회 운영을 위하여 위 일정은 변동될 수 있음을 안내드립니다○ 상금 및 특전 (총 상금 1,400만원 + 특전)- 대상 1팀: 500만원 / 과학기술정보통신부장관상- 우수상 2팀: 각 200만원 / 과학기술정보통신부장관상- 장려상 4팀: 각 100만원 / 한국전자통신연구원장상- 가작 (Poster 세션) 2팀: 각 50만원 / 한국전자통신연구원장상※ 상금은 제세공과금 제외 후 지급되며, 팀이 수상하는 경우 팀 대표에게 일괄 지급됩니다○ 문의- 인공지능팩토리: cs@aifactory.page- 한국컴퓨터종합학술대회 (KCC2023): 추후 안내
-
10주완성 C++ 코딩테스트 | 알고리즘 코딩테스트
C++ 교안의 예시 코드를 github에 올려도 될까요?
삭제된 글입니다
-
Express 튜토리얼 : 웹 서비스를 위한 핵심 API
[해결법] Error: req#logout requires a callback function
에러 나오는 이유passport.js 버전이 올라감에 따라 사용법에 변경이 있었기 때문입니다. 해결법아래와 같이 코드를 변경하면 정상동작합니다.router.get('/logout', (req, res, next) => { req.logOut(err => { if (err) { return next(err); } else { console.log('로그아웃됨.'); res.redirect('/'); } }); }); 자세한 설명https://medium.com/passportjs/fixing-session-fixation-b2b68619c51d위 포스트에서 내용 일부발췌하여 간단한 번역을 덧붙여둡니다. The other major change is that that req.logout() is now an asynchronous function, whereas previously it was synchronous. For instance, a logout route that was previously:이번 업데이트로 원래는 동기 함수였던 req.logout()이 비동기 함수가 됐습니다. 바로 아래의 코드는 동기함수였을 시절 쓰던 방식입니다.app.post('/logout', function(req, res, next) { req.logout(); res.redirect('/'); });should be modified to:이젠 위 코드처럼 쓰지 말고, 아래처럼 써야 잘 동작합니다.app.post('/logout', function(req, res, next) { req.logout(function(err) { if (err) { return next(err); } res.redirect('/'); }); }); ...This improves the overall security posture of any app using Passport for authentication.바뀐 사용법은 보안(security)상의 이점이 있습니다.
-
Verilog FPGA Program 1 (Arty A7-35T)
강의진행률 문의
강의진행률이 99.52%에서 넘어가지질 않습니다..문의드립니다.마지막 페이지 212/213에서 끝납니다..
-
스프링 DB 2편 - 데이터 접근 활용 기술
스프링 부트 3.0 기준 Hibernate 6 Logging 설정입니다.
logging.level.org.hibernate.SQL = debug logging.level.org.hibernate.orm.jdbc.bind = trace찾아보니까 Hibernate 6 부터org.hibernate.type.descriptor.sql -> org.hibernate.orm.jdbc.bind 으로 변경되었다고 하네요 ~모두들 화이팅! ^^7참고: https://thorben-janssen.com/hibernate-logging-guide/#Logback_via_Slf4j_configuration_for_Hibernate_4_5_and_6
-
스프링 DB 2편 - 데이터 접근 활용 기술
혹시나 저처럼 스프링 부트 3.0으로 따라오고 계신 분들을 위해...
https://mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/저처럼 스프링 부트 3.0을 사용하고 계신다면, build.gradle에 추가해줄때 3.0.1 버전으로 추가하셔야 합니다. 그 외의 버전은 위 사이트를 참고해주세요.아래의 코드를 복사하시면 됩니다. ^^7 다들 화이팅!implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.1'
-
[NarP Series] MVC 프레임워크는 내 손에 [나프1탄]
감탄밖에 안 나오네요...
MVC의 세 가지 중요 요소(Forward, 객체 바인딩, Request Dispatcher)를 왜 이제야 알았는지...신입인데 너무 늦게 깨달은 건 아닌가 싶으면서도 한 편으로 오늘 배워서 다행이다라는 생각이 듭니다^^
-
데이터 분석 SQL Fundamentals
datagrip에서 복구하기
datagrip에서 복구 하려고 하면 postgre 관련 cli 가 필요합니다. 아래와 같이 우선 실행$ brew install libpq $ echo 'export PATH="/opt/homebrew/opt/libpq/bin:$PATH"' >> ~/.zshrc $ source ~/.zshrc $ psql --version psql (PostgreSQL) 15.2Path to pg_restore 에서 CMD+SHIFT+G 눌러서 brew로 설치한 디렉토리로 이동이후 복구 하면 됩니다
-
실전! 스프링 데이터 JPA
맥북 사양
곧 맥북 구매 예정인데 영한님께서 사용하시는 사양이 궁금합니다. (특히 ram)혹시 알 수 있을까요?
-
언리얼 엔진 5 FPS 게임 만들기
part1 - 14 강의에 오류가 있습니다!
강의 영상에서 게임 모드 블루프린트로 전환한 뒤 게임 실행 영상에서 score가 0으로 UI가 업데이트 되지 않습니다.BP_GM_Part1 블루프린트 클래스 Tick 이벤트 바로 옆의 Update에 Score를 넣어주니 정상적으로 실행되었습니다
-
[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버
C++ 구현한 Iocp 서버 언리얼 연동 궁금
안녕하세요, c++로 iocp server를 구현해서 c++로 만든 client에서 테스트 중 입니다.다만, client는 언리얼로 만들어서 c++ 만든 iocp server 보낸 데이터로 박스도 움직이고 채팅도 확인해보고싶은데 혹시, 연동하는 과정에 관련된 정보 같은것은 어디서 어떻게 확인해볼수 있을까요?구글에 검색하니까 언리얼을 iocp server와 연동하는 정보가 너무 없어서 답답해서 남겨봅니다
-
갖고노는 MySQL 데이터베이스 by 얄코
MacOS에서 MySQL workbench에서 조회할 때 튕기시는 분
sakila db 조회할 때마다 위처럼 튕겨서 찾아보니 MySQL workbench를 8.0.31버전으로 받아야한다고 하네요(참고)다운로드 링크:https://downloads.mysql.com/archives/workbench/
-
스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술
주석 부분이지만 누락 된 거 말씀 드려 봅니다.
PDF 문서 6번 11 페이지 마지막 부분 예제 소스의 주석@PathVariable("userId") String userId -> @PathVariable userId부분에서 "String" 타입명시가 빠졌습니다.