newspeed

ChoRong0824·2025년 2월 14일
0

Web

목록 보기
31/51
post-thumbnail

이제 드디어 협업. 백엔드 5명에서 하는 newspeed.
오늘 2월 14일 오전10시 발제. 오후부터 시작하여서, 다음주 목요일 10시 전에 마무리 해야하니,
얼추 평일 기준 5일 안에 구현해야합니다 🔥
마감 기한이 있어 더욱 좋은 것 같습니다 ㅎㅎ

잘못된 브랜치에서 작업중인 경우


ㅋㅋㅋㅋㅋ난 바보다..
집중한 나머지원래 나는 feat/post에서 개발해야했는데,
모르고 main에서 작업하고 있었습니다..ㅋㅋㅋㅋㅋㅋㅋㅋㅋ
main 보자마자 까무짝 놀라서 feat/post로 이동부터했고 바로 무의식중에 add 해버렸습니다 ㅋㅋㅋㅋ
역시나 브랜치 이동했기때문에, 깃은 연동된 상태여서 방금 제가 작업한 내용은 없습니다.
따라서 현재 코드를 적용하기 위해서 누구보다 빠르게, 아무도 모르게 남들보다 빠르게 후다닥

  1. 변경사항을 stash로 저장해주고.
    git stash push -m "fix:move changes to feat/post"
  2. 올바른 저의 작업중인 브랜치로 이동.
  3. git stash pop으로 stash로 변경된 내용들 적용

이제 원래대로 깃헙에 올리면 됩니다.. ㅎㅎ

완료~~~


저는 게시물(Post) 부분을 담당하게 되어 해당 기능을 구현하던 중, User 부분이 메인 브랜치에 올라온 것을 확인했습니다. User 기능이 얼추 완료된 것 같아, 제 작업과 연동하여 postman으로 테스트를 해보려고 git pull origin main 명령어를 실행했습니다.

그런데, application.properties 파일에서 변경 충돌이 발생하여 에러가 떴습니다.

정리하자면, 로컬에서 application.properties 파일을 수정한 상태에서 커밋을 하지 않은 채로 pull을 시도했기 때문에, Git이 로컬 변경 사항이 원격 변경 내용에 의해 덮어쓰일 수 있다는 이유로 pull을 막은 것이었습니다.


해결방법

로컬 변경 사항을 버리고 main을 최신으로 업데이트.

git restore newspeed/src/main/resources/application.properties
git pull origin main

여기서 restore : 로컬에서 변경된 내용 삭제 후, 원격 버전으로 복원하는 것이다.

그리고 application.properties 파일은 환경 설정 파일로,
로컬 환경에 따라 다를 수 있으니 .gitignore에 추가해줬따.

/src/main/resources/application.properties

추가적인 꿀 팁.

로컬에서 수정한 내용이 필요 없다면? (필요 x 변경) -> 변경 사항을 삭제하고 pull

# 로컬 변경 내용 삭제
git restore newspeed/src/main/resources/application.properties

# main 브랜치로 변경
git checkout main

# main 브랜치 최신화
git pull origin main

# 다시 feat/post로 변경
git checkout feat/post

# main 브랜치 변경 내용 가져오기
git merge main

로컬에서 수정한 내용이 필요 하다면 ?
변경 내용이 필요하다면, 작업 내용을 임시로 저장(stash)하고 이후에 다시 적용하면 됩니다.

# 현재 변경 사항을 스태시로 저장
git stash push -m "application.properties 변경 내용 임시 저장"

# main 브랜치로 이동 및 최신 코드 가져오기
git checkout main
git pull origin main

# feat/post 브랜치로 이동
git checkout feat/post

# main 브랜치 변경 사항 가져오기
git merge main

# 스태시된 변경 사항 복원
git stash pop

게시물 생성 요청 에러

게시물 생성 요청을 보냈는데,

{
    "timestamp": "2025-02-14T12:08:40.655+00:00",
    "status": 500,
    "error": "Internal Server Error",
    "path": "/posts"
}

이런에러가 발생했으며,
콘솔창에는

2025-02-14T21:08:40.626+09:00  WARN 21878 --- [newspeed] [nio-8080-exec-1] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 1048, SQLState: 23000
2025-02-14T21:08:40.626+09:00 ERROR 21878 --- [newspeed] [nio-8080-exec-1] o.h.engine.jdbc.spi.SqlExceptionHelper   : Column 'user_id' cannot be null
2025-02-14T21:08:40.640+09:00 ERROR 21878 --- [newspeed] [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.dao.DataIntegrityViolationException: could not execute statement [Column 'user_id' cannot be null] [/* insert for com.example.newspeed.post.entity.Post */insert into post (content,created_at,image_url,nickname2,title,updated_at,user_id) values (?,?,?,?,?,?,?)]; SQL [/* insert for com.example.newspeed.post.entity.Post */insert into post (content,created_at,image_url,nickname2,title,updated_at,user_id) values (?,?,?,?,?,?,?)]; constraint [null]] with root cause

java.sql.SQLIntegrityConstraintViolationException: Column 'user_id' cannot be null
	at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:109) ~[mysql-connector-j-9.1.0.jar:9.1.0]
	at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:114) ~[mysql-connector-j-9.1.0.jar:9.1.0]
	at com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal(ClientPreparedStatement.java:988) ~[mysql-connector-j-9.1.0.jar:9.1.0]
	at com.mysql.cj.jdbc.ClientPreparedStatement.executeUpdateInternal(ClientPreparedStatement.java:1166) ~[mysql-connector-j-9.1.0.jar:9.1.0]
	at com.mysql.cj.jdbc.ClientPreparedStatement.executeUpdateInternal(ClientPreparedStatement.java:1101) ~[mysql-connector-j-9.1.0.jar:9.1.0]
	at com.mysql.cj.jdbc.ClientPreparedStatement.executeLargeUpdate(ClientPreparedStatement.java:1467) ~[mysql-connector-j-9.1.0.jar:9.1.0]
	at com.mysql.cj.jdbc.ClientPreparedStatement.executeUpdate(ClientPreparedStatement.java:1084) ~[mysql-connector-j-9.1.0.jar:9.1.0]
	at com.zaxxer.hikari.pool.ProxyPreparedStatement.executeUpdate(ProxyPreparedStatement.java:61) ~[HikariCP-5.1.0.jar:na]
	at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.executeUpdate(HikariProxyPreparedStatement.java) ~[HikariCP-5.1.0.jar:na]
	at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.executeUpdate(ResultSetReturnImpl.java:194) ~[hibernate-core-6.6.5.Final.jar:6.6.5.Final]
	at org.hibernate.id.insert.GetGeneratedKeysDelegate.performMutation(GetGeneratedKeysDelegate.java:116) ~[hibernate-core-6.6.5.Final.jar:6.6.5.Final]
	at org.hibernate.engine.jdbc.mutation.internal.MutationExecutorSingleNonBatched.performNonBatchedOperations(MutationExecutorSingleNonBatched.java:47) ~[hibernate-core-6.6.5.Final.jar:6.6.5.Final]
	at org.hibernate.engine.jdbc.mutation.internal.AbstractMutationExecutor.execute(AbstractMutationExecutor.java:55) ~[hibernate-core-6.6.5.Final.jar:6.6.5.Final]
	at org.hibernate.persister.entity.mutation.InsertCoordinatorStandard.doStaticInserts(InsertCoordinatorStandard.java:194) ~[hibernate-core-6.6.5.Final.jar:6.6.5.Final]
	at org.hibernate.persister.entity.mutation.InsertCoordinatorStandard.coordinateInsert(InsertCoordinatorStandard.java:132) ~[hibernate-core-6.6.5.Final.jar:6.6.5.Final]
	at org.hibernate.persister.entity.mutation.InsertCoordinatorStandard.insert(InsertCoordinatorStandard.java:95) ~[hibernate-core-6.6.5.Final.jar:6.6.5.Final]
	at org.hibernate.action.internal.EntityIdentityInsertAction.execute(EntityIdentityInsertAction.java:85) ~[hibernate-core-6.6.5.Final.jar:6.6.5.Final]
	at org.hibernate.engine.spi.ActionQueue.execute(ActionQueue.java:682) ~[hibernate-core-6.6.5.Final.jar:6.6.5.Final]
	at org.hibernate.engine.spi.ActionQueue.addResolvedEntityInsertAction(ActionQueue.java:293) ~[hibernate-core-6.6.5.Final.jar:6.6.5.Final]
	at org.hibernate.engine.spi.ActionQueue.addInsertAction(ActionQueue.java:274) ~[hibernate-core-6.6.5.Final.jar:6.6.5.Final]
	at org.hibernate.engine.spi.ActionQueue.addAction(ActionQueue.java:324) ~[hibernate-core-6.6.5.Final.jar:6.6.5.Final]
	at org.hibernate.event.internal.AbstractSaveEventListener.addInsertAction(AbstractSaveEventListener.java:393) ~[hibernate-core-6.6.5.Final.jar:6.6.5.Final]
	at org.hibernate.event.internal.AbstractSaveEventListener.performSaveOrReplicate(AbstractSaveEventListener.java:307) ~[hibernate-core-6.6.5.Final.jar:6.6.5.Final]
	at org.hibernate.event.internal.AbstractSaveEventListener.performSave(AbstractSaveEventListener.java:223) ~[hibernate-core-6.6.5.Final.jar:6.6.5.Final]
	at org.hibernate.event.internal.AbstractSaveEventListener.saveWithGeneratedId(AbstractSaveEventListener.java:136) ~[hibernate-core-6.6.5.Final.jar:6.6.5.Final]
	at org.hibernate.event.internal.DefaultPersistEventListener.entityIsTransient(DefaultPersistEventListener.java:177) ~[hibernate-core-6.6.5.Final.jar:6.6.5.Final]
	at org.hibernate.event.internal.DefaultPersistEventListener.persist(DefaultPersistEventListener.java:95) ~[hibernate-core-6.6.5.Final.jar:6.6.5.Final]
	at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:79) ~[hibernate-core-6.6.5.Final.jar:6.6.5.Final]
	at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:55) ~[hibernate-core-6.6.5.Final.jar:6.6.5.Final]
	at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:127) ~[hibernate-core-6.6.5.Final.jar:6.6.5.Final]
	at org.hibernate.internal.SessionImpl.firePersist(SessionImpl.java:761) ~[hibernate-core-6.6.5.Final.jar:6.6.5.Final]
	at org.hibernate.internal.SessionImpl.persist(SessionImpl.java:745) ~[hibernate-core-6.6.5.Final.jar:6.6.5.Final]
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[na:na]
	at java.base/java.lang.reflect.Method.invoke(Method.java:577) ~[na:na]
	at org.springframework.orm.jpa.ExtendedEntityManagerCreator$ExtendedEntityManagerInvocationHandler.invoke(ExtendedEntityManagerCreator.java:364) ~[spring-orm-6.2.2.jar:6.2.2]
	at jdk.proxy2/jdk.proxy2.$Proxy133.persist(Unknown Source) ~[na:na]
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[na:na]
	at java.base/java.lang.reflect.Method.invoke(Method.java:577) ~[na:na]
	at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:320) ~[spring-orm-6.2.2.jar:6.2.2]
	at jdk.proxy2/jdk.proxy2.$Proxy133.persist(Unknown Source) ~[na:na]
	at org.springframework.data.jpa.repository.support.SimpleJpaRepository.save(SimpleJpaRepository.java:627) ~[spring-data-jpa-3.4.2.jar:3.4.2]
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[na:na]
	at java.base/java.lang.reflect.Method.invoke(Method.java:577) ~[na:na]
	at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:359) ~[spring-aop-6.2.2.jar:6.2.2]
	at org.springframework.data.repository.core.support.RepositoryMethodInvoker$RepositoryFragmentMethodInvoker.lambda$new$0(RepositoryMethodInvoker.java:277) ~[spring-data-commons-3.4.2.jar:3.4.2]
	at org.springframework.data.repository.core.support.RepositoryMethodInvoker.doInvoke(RepositoryMethodInvoker.java:170) ~[spring-data-commons-3.4.2.jar:3.4.2]
	at org.springframework.data.repository.core.support.RepositoryMethodInvoker.invoke(RepositoryMethodInvoker.java:158) ~[spring-data-commons-3.4.2.jar:3.4.2]
	at org.springframework.data.repository.core.support.RepositoryComposition$RepositoryFragments.invoke(RepositoryComposition.java:515) ~[spring-data-commons-3.4.2.jar:3.4.2]
	at org.springframework.data.repository.core.support.RepositoryComposition.invoke(RepositoryComposition.java:284) ~[spring-data-commons-3.4.2.jar:3.4.2]
	at org.springframework.data.repository.core.support.RepositoryFactorySupport$ImplementationMethodExecutionInterceptor.invoke(RepositoryFactorySupport.java:752) ~[spring-data-commons-3.4.2.jar:3.4.2]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.2.2.jar:6.2.2]
	at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.doInvoke(QueryExecutorMethodInterceptor.java:174) ~[spring-data-commons-3.4.2.jar:3.4.2]
	at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.invoke(QueryExecutorMethodInterceptor.java:149) ~[spring-data-commons-3.4.2.jar:3.4.2]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.2.2.jar:6.2.2]
	at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:69) ~[spring-data-commons-3.4.2.jar:3.4.2]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.2.2.jar:6.2.2]
	at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:380) ~[spring-tx-6.2.2.jar:6.2.2]
	at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119) ~[spring-tx-6.2.2.jar:6.2.2]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.2.2.jar:6.2.2]
	at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:138) ~[spring-tx-6.2.2.jar:6.2.2]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.2.2.jar:6.2.2]
	at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:165) ~[spring-data-jpa-3.4.2.jar:3.4.2]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.2.2.jar:6.2.2]
	at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:223) ~[spring-aop-6.2.2.jar:6.2.2]
	at jdk.proxy2/jdk.proxy2.$Proxy136.save(Unknown Source) ~[na:na]
	at com.example.newspeed.post.service.PostService.createPost(PostService.java:35) ~[main/:na]
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[na:na]
	at java.base/java.lang.reflect.Method.invoke(Method.java:577) ~[na:na]
	at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:359) ~[spring-aop-6.2.2.jar:6.2.2]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:196) ~[spring-aop-6.2.2.jar:6.2.2]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-6.2.2.jar:6.2.2]
	at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:380) ~[spring-tx-6.2.2.jar:6.2.2]
	at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119) ~[spring-tx-6.2.2.jar:6.2.2]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.2.2.jar:6.2.2]
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:727) ~[spring-aop-6.2.2.jar:6.2.2]
	at com.example.newspeed.post.service.PostService$$SpringCGLIB$$0.createPost(<generated>) ~[main/:na]
	at com.example.newspeed.post.controller.PostController.createPost(PostController.java:31) ~[main/:na]
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[na:na]
	at java.base/java.lang.reflect.Method.invoke(Method.java:577) ~[na:na]
	at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:257) ~[spring-web-6.2.2.jar:6.2.2]
	at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:190) ~[spring-web-6.2.2.jar:6.2.2]
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:118) ~[spring-webmvc-6.2.2.jar:6.2.2]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:986) ~[spring-webmvc-6.2.2.jar:6.2.2]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:891) ~[spring-webmvc-6.2.2.jar:6.2.2]
	at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-6.2.2.jar:6.2.2]
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1088) ~[spring-webmvc-6.2.2.jar:6.2.2]
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:978) ~[spring-webmvc-6.2.2.jar:6.2.2]
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014) ~[spring-webmvc-6.2.2.jar:6.2.2]
	at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:914) ~[spring-webmvc-6.2.2.jar:6.2.2]
	at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:590) ~[tomcat-embed-core-10.1.34.jar:6.0]
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885) ~[spring-webmvc-6.2.2.jar:6.2.2]
	at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658) ~[tomcat-embed-core-10.1.34.jar:6.0]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
	at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51) ~[tomcat-embed-websocket-10.1.34.jar:10.1.34]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
	at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-6.2.2.jar:6.2.2]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.2.2.jar:6.2.2]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
	at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-6.2.2.jar:6.2.2]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.2.2.jar:6.2.2]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
	at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-6.2.2.jar:6.2.2]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.2.2.jar:6.2.2]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
	at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
	at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:397) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:905) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1741) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
	at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1190) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
	at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
	at java.base/java.lang.Thread.run(Thread.java:833) ~[na:na]

Hibernate: 
    /* <criteria> */ select
        p1_0.post_id,
        p1_0.content,
        p1_0.created_at,
        p1_0.image_url,
        p1_0.nickname2,
        p1_0.title,
        p1_0.updated_at,
        p1_0.user_id 
    from
        post p1_0
Hibernate: 
    select
        p1_0.post_id,
        p1_0.content,
        p1_0.created_at,
        p1_0.image_url,
        p1_0.nickname2,
        p1_0.title,
        p1_0.updated_at,
        p1_0.user_id 
    from
        post p1_0 
    where
        p1_0.post_id=?
2025-02-14T21:09:09.753+09:00 ERROR 21878 --- [newspeed] [nio-8080-exec-5] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: java.lang.IllegalArgumentException: 게시글을 찾을 수 없습니다.] with root cause

java.lang.IllegalArgumentException: 게시글을 찾을 수 없습니다.
	at com.example.newspeed.post.service.PostService.lambda$getPostById$0(PostService.java:28) ~[main/:na]
	at java.base/java.util.Optional.orElseThrow(Optional.java:403) ~[na:na]
	at com.example.newspeed.post.service.PostService.getPostById(PostService.java:28) ~[main/:na]
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[na:na]
	at java.base/java.lang.reflect.Method.invoke(Method.java:577) ~[na:na]
	at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:359) ~[spring-aop-6.2.2.jar:6.2.2]
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:723) ~[spring-aop-6.2.2.jar:6.2.2]
	at com.example.newspeed.post.service.PostService$$SpringCGLIB$$0.getPostById(<generated>) ~[main/:na]
	at com.example.newspeed.post.controller.PostController.getPost(PostController.java:26) ~[main/:na]
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[na:na]
	at java.base/java.lang.reflect.Method.invoke(Method.java:577) ~[na:na]
	at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:257) ~[spring-web-6.2.2.jar:6.2.2]
	at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:190) ~[spring-web-6.2.2.jar:6.2.2]
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:118) ~[spring-webmvc-6.2.2.jar:6.2.2]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:986) ~[spring-webmvc-6.2.2.jar:6.2.2]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:891) ~[spring-webmvc-6.2.2.jar:6.2.2]
	at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-6.2.2.jar:6.2.2]
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1088) ~[spring-webmvc-6.2.2.jar:6.2.2]
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:978) ~[spring-webmvc-6.2.2.jar:6.2.2]
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014) ~[spring-webmvc-6.2.2.jar:6.2.2]
	at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:903) ~[spring-webmvc-6.2.2.jar:6.2.2]
	at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:564) ~[tomcat-embed-core-10.1.34.jar:6.0]
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885) ~[spring-webmvc-6.2.2.jar:6.2.2]
	at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658) ~[tomcat-embed-core-10.1.34.jar:6.0]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
	at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51) ~[tomcat-embed-websocket-10.1.34.jar:10.1.34]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
	at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-6.2.2.jar:6.2.2]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.2.2.jar:6.2.2]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
	at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-6.2.2.jar:6.2.2]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.2.2.jar:6.2.2]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
	at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-6.2.2.jar:6.2.2]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.2.2.jar:6.2.2]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
	at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
	at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:397) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:905) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1741) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
	at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1190) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
	at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
	at java.base/java.lang.Thread.run(Thread.java:833) ~[na:na]

에러가 발생했다.

잘 보면, Column 'user_id' cannot be null 을 볼 수 있을 것입니다.

Post 엔티티에는 user_id가 @ManyToOne 관계로 필수(FK)로 정의되어 있습니다. 하지만 게시물 생성 시 user_id를 null로 전달하므로 DB에서 무결성 제약 조건 위반 발생한 것입니다.

  • 엔티티에 변수 추가해주고, 테스트때 notnull 조건인 것들에 대해 request 하면 됩니다.
    (테스트 시, 정상작동 됌)

이젠 몇 가지 수정하려합니다. - 2025.02.17

  1. 게시물 작성, 조회, 수정, 삭제 기능
  • 수정 및 삭제는 작성자 본인만 가능 (Authorization 체크)
  1. 뉴스피드 조회 기능
  • 생성일자 기준 내림차순 정렬
  • 10개씩 페이지네이션

WHY? 수정해야할까 ?

  1. 보안 및 무결성
    다른 사용자가 타인의 게시물을 수정/삭제할 수 있으면 보안 문제가 발생하기 떄문입니다.
  2. 사용자 경험 향상
    최신 게시물이 상단에 노출되도록 정렬해야 사용자가 쉽게 최신 소식을 확인할 수 있어 좋습니다.
  3. 성능 최적화
    페이지네이션은 데이터가 많을 때 성능을 높이고, 서버 부담을 줄여 줍니다.

코드 수정하고, 테스트 해보니까

이 발생했습니다.
401 Unauthorized는 Spring Security에서 인증되지 않은 요청을 차단할 때 발생합니다.
Spring Security 설정이 추가되었는지 확인

정확히는 회원가입 후, 게시물 관련 정상 동작하였으나,
이번에 시도시 401Unauthorized 발생해서 Spring Security 설정 추가해서 필요한 코드 추가하겠습니다.

로 알았는데, 제가 회원가입만 하고, 로그인 안하고 테스트 해서 이렇게 된 것입니다.
회원가입 -> 로그인 -> 게시글 작성 -> 정상 동작 확인.


그러나 나는 큰 코드 에러를 범하고 있었다.

public ResponseEntity<PostResponse> updatePost(
            @PathVariable Long postId,
            @RequestParam Long userId, // 요청 파라미터에 userId 추가
            @RequestBody @Valid PostRequest request) {
        return ResponseEntity.ok(postService.updatePost(postId, userId, request));
    }

    @DeleteMapping("/{postId}")
    public ResponseEntity<Void> deletePost(
            @PathVariable Long postId,
            @RequestParam Long userId) {
        postService.deletePost(postId, userId);
        return ResponseEntity.noContent().build();
    }

바보다.
이렇게 userId를 매개변수로 받게되면, client가 임의로 조작 가능 하기 때문에 절대 안되는 코드라는 것을 작성당시 인지하지 못했다.
이건 코드 리뷰 할 때 잘못하면 욕 먹을 각오도 해야되는 수준이라고 생각헀다.
다행히도 혼자 코드리뷰 하면서 발견하게 되서 다행이다.
후다닥 수정하려한다.
라이언트가 요청 파라미터로 userId를 임의로 조작할 수 있기 때문에, 보안상 매우 취약한 방식임으로 코드 수정.
해결 방법으로는 여러 가지가 있다.
일단, 인증 정보 사용(Spring SEcurity를 사용하는 것이다)

  • Spring Security 같은 인증 프레임워크를 사용하여 현재 로그인한 사용자 정보를 서버에서 가져와야 합니다.
  • 예를 들어, 컨트롤러 메서드에서 Principal 또는 @AuthenticationPrincipal을 사용하면, 클라이언트가 조작할 수 없는 인증 정보를 바탕으로 userId를 결정할 수 있습니다

그러나 나는 Spring Security를 사용하지 않고 구현하고 싶었다.

그럼 방법이 없는 것일까?
아니다. 있다. 처음부터 스프링 시큐리티가 있었던 것은 아니기 때문이다.

Spring Security 없이 인증을 구현하는 경우, 클라이언트가 전달하는 userId를 신뢰할 수 없으므로 서버에서 사용자 인증 정보를 별도로 관리해야 합니다. 대표적인 방법은 로그인 시 서버의 HttpSession에 사용자 정보를 저장하고, 이후 요청에서는 session에서 인증 정보를 꺼내 사용하는 것입니다


이해를 돕고자 한 번 더 정리하겠습니다. 근데 왜 userId를 매개변수로 받게되면, client가 임의로 조작 가능 하기 때문에 절대 안되는 코드인 것일까? 어떻게 임의로 조작가능하다는거지?? 이 말을 아예 이해하기 어렵지 않나요?.

좀 이해하기 쉽게 설명하면

클라이언트가 요청에 포함하는 모든 데이터는 신뢰할 수 없는 입력입니다. 예를 들어, 만약 update나 delete 요청에 userId를 매개변수로 받는다면, 클라이언트는 자신의 실제 userId가 아닌 다른 사용자의 userId를 임의로 넣어서 서버에 요청을 보낼 수 있습니다.

(예시)
원래 A라는 사용자가 userId 1을 가지고 있고, 본인의 게시글만 수정할 수 있도록 설계되어 있다고 가정합시다.
그런데 클라이언트가 직접 userId 파라미터를 전달하도록 하면, 만약 사용자가 B라고 해도 userId 값을 1로 변경하여 요청을 보낼 수 있습니다.
그러면 서버는 "userId 1이 게시글의 작성자이다"라고 검증할 때 B 사용자가 아닌 A 사용자의 게시글에 접근하도록 허용할 위험이 생깁니다.
즉, 클라이언트가 userId를 보내도록 하면 사용자가 자신이 가진 정보 이외의 값을 조작하여 보내버릴 수 있기 때문에, 인증된 사용자 정보를 서버에서 직접 얻어야 합니다. 이를 위해 로그인 시 세션이나 토큰을 사용하면, 클라이언트가 임의로 수정할 수 없는 안전한 방식으로 userId를 확보할 수 있습니다.

이게 바로 "클라이언트가 임의로 조작 가능"하다는 의미입니다.


그럼 이제 코드를 수정해봅시다.

  1. 인증 방식 변경
    GET, PUT, DELETE, PATCH 등 모든 API에서 클라이언트가 userId를 직접 전달하는 대신, 로그인 시 서버의 HttpSession에 저장된 userId를 사용합니다.
  2. 친구 필터링 기능 추가 (뉴스피드 조회)
    뉴스피드 조회 시, 로그인한 사용자의 친구 목록을 friendService를 통해 조회한 후, 그 친구들이 작성한 게시글만 반환하도록 합니다.
  3. 컨트롤러와 서비스 모두 수정
    컨트롤러: 모든 API에서 HttpSession에서 userId를 가져오도록 수정
    서비스: getAllPosts 메서드에서 friend filtering 로직을 추가
package com.example.newspeed.post.controller;

import com.example.newspeed.post.dto.request.PostRequest;
import com.example.newspeed.post.dto.request.PostStatusRequest;
import com.example.newspeed.post.dto.response.PostResponse;
import com.example.newspeed.post.service.PostService;
import jakarta.servlet.http.HttpSession;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/posts")
@RequiredArgsConstructor
public class PostController {
    private final PostService postService;

    // 뉴스피드 조회 (친구 게시글만, 생성일 내림차순, 10개씩 페이징)
    @GetMapping
    public ResponseEntity<Page<PostResponse>> getAllPosts(
            @RequestParam(defaultValue = "0") int page,
            HttpSession session) {
        Long userId = (Long) session.getAttribute("userId");
        if (userId == null) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        }
        return ResponseEntity.ok(postService.getAllPosts(page, userId));
    }

    // 게시글 단건 조회 (삭제된 게시글은 조회 불가)
    @GetMapping("/{postId}")
    public ResponseEntity<PostResponse> getPost(@PathVariable("postId") Long postId) {
        return ResponseEntity.ok(postService.getPostById(postId));
    }

    // 게시글 수정 - HttpSession에서 인증된 userId 사용
    @PutMapping("/{postId}")
    public ResponseEntity<PostResponse> updatePost(
            @PathVariable Long postId,
            @RequestBody @Valid PostRequest request,
            HttpSession session) {
        Long userId = (Long) session.getAttribute("userId");
        if (userId == null) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        }
        return ResponseEntity.ok(postService.updatePost(postId, userId, request));
    }

    // 게시글 삭제 (실제로는 상태를 DELETED로 변경) - HttpSession 사용
    @DeleteMapping("/{postId}")
    public ResponseEntity<Void> deletePost(
            @PathVariable Long postId,
            HttpSession session) {
        Long userId = (Long) session.getAttribute("userId");
        if (userId == null) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        }
        postService.deletePost(postId, userId);
        return ResponseEntity.noContent().build();
    }

    // 게시글 상태 변경 (공개/비공개/삭제) - HttpSession 사용
    @PatchMapping("/{postId}/status")
    public ResponseEntity<PostResponse> changeStatus(
            @PathVariable Long postId,
            @RequestBody @Valid PostStatusRequest statusRequest,
            HttpSession session) {
        Long userId = (Long) session.getAttribute("userId");
        if (userId == null) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        }
        return ResponseEntity.ok(postService.changeStatus(postId, userId, statusRequest));
    }
}
package com.example.newspeed.post.service;

import com.example.newspeed.post.dto.request.PostRequest;
import com.example.newspeed.post.dto.request.PostStatusRequest;
import com.example.newspeed.post.dto.response.PostResponse;
import com.example.newspeed.post.entity.Post;
import com.example.newspeed.post.entity.PostStatus;
import com.example.newspeed.post.repository.PostRepository;
import com.example.newspeed.user.repository.UserRepository;
import jakarta.persistence.EntityNotFoundException;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;

import java.util.List;

@Service
@RequiredArgsConstructor
public class PostService {

    private final PostRepository postRepository;
    private final UserRepository userRepository;
    private final FriendService friendService; // 친구 필터링을 위한 서비스

    // 뉴스피드 조회 - 로그인한 사용자의 친구들이 작성한 게시글만 반환 (생성일 내림차순, 10개씩 페이징)
    @Transactional(readOnly = true)
    public Page<PostResponse> getAllPosts(int page, Long userId) {
        Pageable pageable = PageRequest.of(page, 10, Sort.by(Sort.Direction.DESC, "createdAt"));
        // 로그인한 사용자의 친구 ID 목록을 조회
        List<Long> friendIds = friendService.findFriendIdsByUserId(userId);
        // 친구들이 작성한 게시글만 조회 (friendIds가 없는 경우 빈 리스트로 처리)
        if (friendIds.isEmpty()) {
            return Page.empty(pageable);
        }
        return postRepository.findByUserIdIn(friendIds, pageable)
                .map(this::mapToResponse);
    }

    public PostResponse getPostById(Long postId) {
        Post post = postRepository.findById(postId)
                .orElseThrow(() -> new EntityNotFoundException("게시글을 찾을 수 없습니다."));
        // DELETED 상태이면 조회 불가
        if (post.getStatus() == PostStatus.DELETED) {
            throw new ResponseStatusException(HttpStatus.NOT_FOUND, "삭제된 게시글입니다.");
        }
        return mapToResponse(post);
    }

    // 게시글 수정 (삭제 상태인 게시글은 수정 불가)
    @Transactional
    public PostResponse updatePost(Long postId, Long userId, PostRequest request) {
        Post post = postRepository.findById(postId)
                .orElseThrow(() -> new EntityNotFoundException("게시글을 찾을 수 없습니다."));

        if (post.getStatus() == PostStatus.DELETED) {
            throw new ResponseStatusException(HttpStatus.FORBIDDEN, "삭제된 게시글은 수정할 수 없습니다.");
        }
        if (!post.isOwner(userId)) {
            throw new ResponseStatusException(HttpStatus.FORBIDDEN, "작성자만 수정할 수 있습니다.");
        }

        post.update(
                request.title(),
                request.content(),
                request.imageUrl(),
                request.nickname2()
        );
        return mapToResponse(post);
    }

    // 게시글 삭제 (실제 삭제 대신 DELETED 상태로 변경)
    @Transactional
    public void deletePost(Long postId, Long userId) {
        Post post = postRepository.findById(postId)
                .orElseThrow(() -> new EntityNotFoundException("게시글을 찾을 수 없습니다."));
        if (!post.isOwner(userId)) {
            throw new ResponseStatusException(HttpStatus.FORBIDDEN, "작성자만 삭제할 수 있습니다.");
        }
        post.changeStatus(PostStatus.DELETED);
    }

    // 게시글 상태 변경 (공개/비공개/삭제)
    @Transactional
    public PostResponse changeStatus(Long postId, Long userId, PostStatusRequest statusRequest) {
        Post post = postRepository.findById(postId)
                .orElseThrow(() -> new EntityNotFoundException("게시글을 찾을 수 없습니다."));
        if (!post.isOwner(userId)) {
            throw new ResponseStatusException(HttpStatus.FORBIDDEN, "작성자만 상태를 변경할 수 있습니다.");
        }
        PostStatus newStatus;
        try {
            newStatus = PostStatus.valueOf(statusRequest.status().toUpperCase());
        } catch (IllegalArgumentException e) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "유효하지 않은 상태 값입니다.");
        }
        post.changeStatus(newStatus);
        return mapToResponse(post);
    }

    private PostResponse mapToResponse(Post post) {
        return new PostResponse(
                post.getPostId(),
                post.getTitle(),
                post.getContent(),
                post.getImageUrl(),
                post.getNickname2(),
                post.getCreatedAt(),
                post.getUpdatedAt()
        );
    }
}


jwt 적용해서 로그인 테스트 중에, 위와 같은 에러가 발생했다.
이런 에러는
보통 500 에러는 내부에서 예외가 발생했음을 의미하는데, 최근 JJWT 라이브러리에서는 서명 키를 지정하는 방식이 변경되었습니다.
즉, 기존처럼 단순히 문자열을 전달하는 대신, 문자열을 바이트 배열로 변환하여 암호화 키(Key 객체)로 만들어야 합니다.

return Jwts.builder()
        .setSubject(email)
        .setIssuedAt(new Date())
        .setExpiration(new Date(System.currentTimeMillis() + jwtExpirationMs))
        .signWith(SignatureAlgorithm.HS512, jwtSecret)
        .compact();

JJWT 0.11.5 버전부터는 다음과 같이 변경해야 합니다:

import io.jsonwebtoken.security.Keys;
import java.nio.charset.StandardCharsets;

return Jwts.builder()
        .setSubject(email)
        .setIssuedAt(new Date())
        .setExpiration(new Date(System.currentTimeMillis() + jwtExpirationMs))
        .signWith(Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8)), SignatureAlgorithm.HS512)
        .compact();

이렇게 하면, jwtSecret 문자열을 안전하게 Key 객체로 변환해서 사용할 수 있으므로 서명 과정에서 발생하는 예외를 방지할 수 있습니다.

따라서, 500 에러의 원인은 JWT 토큰 생성 시 서명 키를 올바르게 지정하지 않아 발생한 문제일 가능성이 큽니다. 위와 같이 수정한 후 다시 테스트하면 됩니다.

또는
The signing key's size is not sufficient 라는 문구가 보이면,
JWT 토큰 서명에 사용하는 jwt.secret 값의 길이 문제일 가능성이 있을 수 잇습니다.
HS512알고리즘은 최소 512비트, 64바이트의 비밀키가 필요하기 때문입니다.

즉, 충분히 긴지 확인해야합니다. 64바이트보다 짧다면 500에러가 발생할 수 있으니 수정하면 됩니다.
따라서 저는
jwt.secret=2r81SMs1eJjT9fK2zLskd8f7eU7k3F6B9nN4vP0qC3rX8sY1aD4vG6pJ7lM9nR2x 로 수정해줬습니다.


정상적으로 동작함을 확인했습니다.

~플젝 ㄲㅡㅅ ~ 😁

profile
백엔드를 지향하며, 컴퓨터공학과를 졸업한 취준생입니다. 많이 부족하지만 열심히 노력해서 실력을 갈고 닦겠습니다. 부족하고 틀린 부분이 있을 수도 있지만 이쁘게 봐주시면 감사하겠습니다. 틀린 부분은 댓글 남겨주시면 제가 따로 학습 및 자료를 찾아봐서 제 것으로 만들도록 하겠습니다. 귀중한 시간 방문해주셔서 감사합니다.

0개의 댓글