Spotbugs는 자바 코드를 분석해서 버그 패턴을 찾아주고, 해결방안을 제시해주는 공개소프트웨어입니다. 시작하셨던 프로젝트들을 납품을 완료하셨거나 하기 직전인분들도 많을텐데, 납품하기전이나 올해 작성해 본 프로젝트를 자가검증을 위해 돌려보고 자신의 소스를 되돌아보는것도 괜찮은 것 같습니다.
해당 프로그램으로 프로젝트를 돌려면 NullPointException이 발생할 수 있는부분이나, Date Class를 Equals로 비교하고 있는 잘못된 문법 String Class를 '==' 비교연산자로 비교하는 문법 IO를 사용하고 닫지 않았거나 null체크도 하지 않고 강제로 close하는 문법등등 문제가 될 수 있는 요소들을 정리해서 등급별로 보여줍니다.
한국인터넷진흥원(a.k.a KISA)에서도 권고하고 있는 프로그램으로 무료이며 이클립스에서 간단하게 마켓에서 설치해서 사용하시면 됩니다.
Spotbugs사용하기
1. 이클립스를 실행하고 마켓에 들어갑니다.
2. Spotbugs를 검색하고 설치합니다.
3. 설치가 완료되면 검사하고 싶은 프로젝트 우클릭후 SpotBugs를 클릭하면 자동으로 검사가 진행됩니다.
4. 검사가 완료되면 항목별로 문제점을 확인할 수 있습니다.
5. SpotBugs Perspective로 가보시면 상세하기 프로젝트의 문제점을 등급별로 표기해줍니다.
소스코드 설명이나 사용 예시를 보여주기 위해 포스팅을 하다보면 종종 소스를 올리는 경우가 있습니다. 과거의 티스토리에 비하면 코드 블럭도 제공하고 좋아졌지만, 웹 기능을 보여주기 위해 HTML, CSS, Javascript(jQuery) 등을 동시에 포스팅 하는 경우에는 소스만 3개의 코드블럭이 들어가서 가독성도 안좋고 스크롤 압박(?)도 생기게 됩니다.
또한, 사용예시를 바로 보여주고 싶은 경우에도 html모드를 들어가서 작성한 소스들을 조합하여 보여줄 수도 있지만 여간 복잡한게 아니고 꼬일염려도 존재하여 개인적으로 꺼려지는 작업인데, 이런 경우 CodePen을 사용하면 작은 영역안에서 탭을 통해 HTML, CSS, Javascript를 처리하고 사용 결과도 볼 수 있습니다.
이전에 Controller부분에서 @PostConstruct 어노테이션을 활용하여 Thread를 생성하여 돌리고 서버를 따로 구동하는 형태로 작성하였는데, Bean등록을 하거나 @Component 어노테이션을 활용해서 등록하고 사용해보는 예제를 확인해보겠습니다.
Bean등록을 통한 설정
servlet-context.xml
<!-- Netty Server 등록 -->
<beans:bean id="nettyServer" class="package명.NettyServer" name="nettyServer"/>
정의한 NettyServer class파일을 등록하여 사용하면 됩니다.
@Component 어노테이션을 통한 설정
@Component
public class NettyServer {
private static final Logger logger = Logger.getLogger(NettyServer.class);
private final int SERVER_PORT = 15500;
private final TestService testService;
@Autowired
public NettyServer(TestService testService) {
this.testService = testService;
}
private int SERVER_PORT;
private ServerBootstrap sbs = new ServerBootstrap();
private EventLoopGroup bossGroup;
private EventLoopGroup workerGroup;
@PostConstruct
public void run() {
logger.info(" ============================= Netty Server Start ============================= ");
bossGroup = new NioEventLoopGroup(20);
workerGroup = new NioEventLoopGroup(20);
sbs.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_TIMEOUT, 5 * 1000)
.childOption(ChannelOption.SO_TIMEOUT, 5 * 1000)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// 응답없는 read상태 확인하는 Handler
ch.pipeline().addLast("idleStateHandler", new IdleStateHandler(5, 0, 0)); //아이들 상태
// 메시지 송신, 수신, 응답없는 이벤트등을 처리할 Handler
ch.pipeline().addLast("socketServerHandler", new NettyServerHandler(testService) ); //직접 동작 할 핸들러
}
});
doConnect();
}
private void doConnect() {
//서버는 Listen상태로 기다려야하는데, 톰캣이 Timeout이 발생함
//이를 방지하기 위해 Thread로 처리한다.
new Thread(new Runnable() {
@Override
public void run() {
try {
ChannelFuture future = sbs.bind(SERVER_PORT).sync();
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
logger.error("InterruptedException", e);
}
}
}).start();
}
@PreDestroy
public void serverDestroy() throws InterruptedException {
logger.info("================ Netty BootStrapServer Destroy() ================");
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
sbs.group().shutdownGracefully();
}
}
xml설정이 어렵다면 작성한 class를 Bean으로 등록해주는 @Component 어노테이션을 활용하면 보다 쉽게 설정이 가능합니다.
추가해주시고 service, dao등 설정이 필요한 부분은 생성자를 통해 주입해줍니다.
이후 @PostConstruct 어노테이션을 활용하여 ServerBootStrap 옵션을 설정하고 구동을 해줍니다.
doConnect()함수에서 쓰레드를 생성하고 특정 포트로 바인딩하도록 설정하였는데, @PostConstruct는 해당 함수가 끝날때까지 대기하도록 설계되어 있는데, 해당부분에서 Netty 서버는 Listen상태로 계속 기다리고 있게됩니다.
톰캣입장에서는 구동 완료되는 시간이 Timeout설정이 되어 있는데 Timeout시간이 될때까지 설정이 끝나질 않으니 Timeout 에러가 발생하게 됩니다. 이부분을 막기위해 Thread를 통해 별도로 구성을 했습니다.
마무리
아직 스프링부트 프로젝트를 제대로 해볼 일이 없어서 아직도 Legacy만 쓰고 있는데, xml의 설정이 늘 어렵고 복잡하며 헷갈린것 같습니다. 이번 글에서 주의할 점은 xml에서 Bean등록을 했다면 Component 어노테이션은 지워야하고, Component 어노테이션으로 등록을 했다면 xml에서 Bean등록을 삭제해주시면 됩니다. 둘 다 설정하여도 동작은 하겠지만 2중으로 등록되면서 오류가 발생할 수 있습니다.
Spring에서 자주 바뀌는 값을 변수값으로 지정하여 개발하게 되면, 값이 변경이 필요한 경우 class파일 자체가 변경되어야 하기에 번거롭게 되는데, 이런 경우를 대비해 프로퍼티 파일을 읽게하여 재기동만으로 설정된 옵션(DB 연결IP, PORT 등)이나 정보등을 변경된 값으로 처리하여 수정 할 수 있습니다.
일반적으로 프로퍼티는 xml 설정을 통해 설정 하고 처리 할 수 있는데, ssh터널링과 같은 방식을 통해 DB를 연결하거나 해야 할때 xml설정으로 프로퍼티를 읽게되면 구동 순서상 에러가 발생할 수 있습니다.
이런 경우 xml에 Bean등록된 설정파일로 읽기가 아닌 자바에서 처리를 하시면 됩니다.
Java로 프로퍼티 파일 읽기
run()메소드에서 외부 메소드를 호출하여 읽고 정보별로 데이터를 얻는 예제입니다.
@Component
public class Test {
private static final Logger logger = Logger.getLogger(Test.class);
private final TestService testService;
@Autowired
public Test(TestService testService) {
this.testService = testService;
}
@PostConstruct
public void run() {
GenericXmlApplicationContext ctx = new GenericXmlApplicationContext();
ConfigurableEnvironment env = ctx.getEnvironment();
MutablePropertySources prop = env.getPropertySources();
try {
prop.addLast(new ResourcePropertySource("classpath:config.properties"));
} catch (IOException e) {
logger.error("IOException", e);
}
// 프로퍼티 정보 얻기
String ip = env.getProperty("db.ip");
String pw = env.getProperty("db.pw");
logger.info("IP : " + ip);
logger.info("pw : " + pw);
}
}
bootstrap handler를 등록할 때IdleStateHandler 라는 메소드를 등록하여 일정 시간동안 수신을 못받거나 응답을 못해주거나 할 때, Idle상태를 감지해주는 핸들러입니다.
idle상태 체크하기
bs.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//idle 등록
ch.pipeline().addLast("idleStateHandler", new IdleStateHandler(5, 0, 0));
NettyClientHandler handler = new NettyClientHandler(msgArr[idx]);
ch.pipeline().addLast("clientHandler", handler);
}
});
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if(evt instanceof IdleStateEvent) {
IdleStateEvent e = (IdleStateEvent) evt;
if(e.state() == IdleState.READER_IDLE) {
//...처리할 동작
}
}
}
해당 핸들러에 등록된 상태가 만족되면 기존 등록하는 핸들러 중userEventTriggered메소드가 동작하게 되고 이 메소드에서 이벤트 종료를 받을 수 있는데 해당 이벤트가IdleStateEvent의 참조 변수인지 확인 후 처리를 하시면 됩니다.
IdleStateHandler(int readerIdleTimeSeconds, int writerIdleTimeSeconds, int allIdleTimeSeconds)
해당 핸들러 파라미터 옵션에 대하여 간단하게 알아보겠습니다.
각 파라미터 순서대로이며 해당 기능을 비활성하기 위해서는 '0'을 입력합니다.
1. readerIdleTimeSeconds : 읽기(수신) 시간으로 해당 시간만큼 동작하지 않으면 트리거가 발생합니다.
2. writerIdleTimeSeconds : 쓰기(발송) 시간으로 해당 시간만큼 동작하지 않으면 트리거가 발생합니다.
3. allIdleTimeSeconds : 읽기, 쓰기 모두를 지정하며 해당 시간만큼 동작하지 않으면 트리거가 발생합니다.
아래는 완성된 총 NettyClient 튜토리얼 소스입니다.
NettyClientAction.java
package com.psw.socket.nettyPrj.netty.client;
public interface NettyClientAction {
public void close(NettyClientHandler handler);
public void receive(NettyClientHandler handler);
}
NettyClient.java
package com.psw.socket.nettyPrj.netty.client;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import org.apache.log4j.Logger;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.timeout.IdleStateHandler;
/**
* NettyClient
* @author psw
*/
public class NettyClient {
private static final Logger logger = Logger.getLogger(NettyClient.class);
private Bootstrap bs = new Bootstrap();
private SocketAddress addr_;
private Channel channel_;
private String[] msgArr;
private int idx;
private int fail_cnt = 0;
private final int FAIL_COUNT_LIMIT = 3;
private NioEventLoopGroup group;
public NettyClient(SocketAddress addr, String[] msgArr) {
this.addr_ = addr;
this.msgArr = msgArr;
}
public NettyClient(String host, int port, String[] msgArr) {
this(new InetSocketAddress(host, port), msgArr);
}
//실제로 동작시킬 메소드 Bootstrap 연결 옵션 설정 및 연결 처리
public void run() {
if(this.addr_ == null) {
logger.error("주소 정보가 없습니다.");
}else if(this.msgArr == null || this.msgArr.length == 0) {
logger.error("보낼 메시지가 없습니다.");
}
group = new NioEventLoopGroup(3);
bs.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.SO_KEEPALIVE, true);
doConnect();
}
private void doConnect() {
handlerSet();
bs.connect(addr_).addListener(new ChannelFutureListener() {
public void operationComplete(ChannelFuture future) throws Exception {
if(future.isSuccess()) {
logger.info("연결 성공");
logger.info(addr_ + " connect()");
channel_ = future.channel();
}else {
future.channel().close(); //실패하면 기존 연결을 종료하고
if(FAIL_COUNT_LIMIT > ++fail_cnt) {
logger.info("연결 실패 " + fail_cnt + "/" + FAIL_COUNT_LIMIT);
bs.connect(addr_).addListener(this); //재연결 처리를 한다.
}else {
logger.info(FAIL_COUNT_LIMIT + "회 연결 초과");
bs.group().shutdownGracefully(); //eventLoop에 등록된 Thread를 종료 처리한다.
}
}
}
});
}
private void handlerSet() {
if(bs != null) {
bs.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast("idleStateHandler", new IdleStateHandler(5, 0, 0));
NettyClientHandler handler = new NettyClientHandler(msgArr[idx]);
handler.setCallBackClientHandler(new NettyClientAction() {
public void close(NettyClientHandler handler) {
//종료 처리 후 더 보낼게 존재한다면 기존 옵션으로 재 연결처리를 하는 콜백 메소드
logger.info("===== 서버가 응답이 없음 강제 종료 처리 =====");
closeAndContinue();
}
public void receive(NettyClientHandler handler) {
//응답 받은 메시지 콜백 메소드
String receiveMsg = handler.getReceiveMsg();
logger.info("callBack receive : "+ receiveMsg);
closeAndContinue();
}
});
ch.pipeline().addLast("clientHandler", handler);
}
});
}
}
private void closeAndContinue() {
try {
channel_.close().sync(); //현재의 채널을 일단 닫는다.
if(msgArr.length > ++idx) { //보낼 메시지가 남았으면 재연결 처리
doConnect();
}else { //보낼 메시지가 없다면 종료
bs.group().shutdownGracefully(); //eventLoop에 등록된 Thread를 종료 처리한다.
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
NettyClientHandler.java
package com.psw.socket.nettyPrj.netty.client;
import org.apache.log4j.Logger;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
public class NettyClientHandler extends ChannelInboundHandlerAdapter{
private static final Logger logger = Logger.getLogger(NettyClientHandler.class);
private String sendMsg;
private int lostCnt_ = 0;
private final int LIMIT_COUNT = 3;
public NettyClientHandler(String msg) {
this.sendMsg = msg;
}
private NettyClientAction action_;
public void setCallBackClientHandler(NettyClientAction action) {
this.action_ = action;
}
private String receiveMsg;
public String getReceiveMsg() {
return this.receiveMsg;
}
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
}
@Override
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
logger.info("채널이 메시지 발송할 준비가 됨.");
msgSend(ctx);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
logger.info("메시지를 받는 메소드.");
ByteBuf buf = (ByteBuf)msg;
int n = buf.readableBytes();
if( n > 0 ) {
byte[] b = new byte[n];
buf.readBytes(b);
//수신메시지 출력
this.receiveMsg = new String( b );
logger.info("handler 수신된 메시지 >" + this.receiveMsg);
if(this.action_ != null) {
action_.receive(NettyClientHandler.this);
}
}
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
logger.info("메시지를 받는 동작이 끝나면 동작하는 메소드.");
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if(evt instanceof IdleStateEvent) {
IdleStateEvent e = (IdleStateEvent) evt;
if(e.state() == IdleState.READER_IDLE) {
if(LIMIT_COUNT > ++lostCnt_) { //n번을 초과하지 않았기때문에 메시지 재발송
msgSend(ctx);
}else { //대기시간 n초를 n번 초과하여 서버와 연결된 채널을 끊음
if(this.action_ != null) {
action_.close(NettyClientHandler.this);
}
}
}
}
}
private void msgSend(ChannelHandlerContext ctx) {
ByteBuf messageBuffer = Unpooled.buffer();
messageBuffer.writeBytes(sendMsg.getBytes());
ctx.writeAndFlush( messageBuffer ); //메시지를 발송하고 flush처리
logger.info("발송 메시지 >" + sendMsg);
}
}
App.java
package com.psw.socket.nettyPrj;
import org.apache.log4j.Logger;
import com.psw.socket.nettyPrj.netty.client.NettyClient;
public class App {
private static final Logger logger = Logger.getLogger(App.class);
public static void main( String[] args ){
logger.info("======= Netty Client Test =======");
String host = "127.0.0.1";
int port = 15510;
String[] msgArr = {"hello world\n", "hello world2\n", "hello world3\n"};
new NettyClient(host, port, msgArr).run();
}
}
동작 결과
처음 네티를 적용해보면서 경험해본 기술들을 정리해보았습니다. 잘 못 된 부분이 있다면 피드백 부탁드립니다. :D
NettyClient에 대해 포스팅을 진행하고 있는데, 첫번째 포스팅 글에서는 10개의 보낼 메시지가 있으면 한번 연결된 상태에서 10개의 메시지를 발신할때까지 핸들러에서 제어하면서 처리를 하였습니다.
다른 진행 방법으로 요청사항으로 메시지별 발송 -> 수신 -> 종료 형태로 반복하여 처리해달라고 하게 되면, 어떤식으로 처리를 해야 할지 알아보겠습니다.
NettyClient
1. NettyClientAction.java
package com.psw.socket.nettyPrj.netty.client;
public interface NettyClientAction {
public void close(NettyClientHandler handler);
public void receive(NettyClientHandler handler);
}
콜백 메소드 처리를 위해 interface 메소드 선언
2. NettyClient.java
package com.psw.socket.nettyPrj.netty.client;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import org.apache.log4j.Logger;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
/**
* NettyClient
* @author psw
*/
public class NettyClient {
private static final Logger logger = Logger.getLogger(NettyClient.class);
private Bootstrap bs = new Bootstrap();
private SocketAddress addr_;
private Channel channel_;
private String[] msgArr;
private int idx;
private NioEventLoopGroup group;
public NettyClient(SocketAddress addr, String[] msgArr) {
this.addr_ = addr;
this.msgArr = msgArr;
}
public NettyClient(String host, int port, String[] msgArr) {
this(new InetSocketAddress(host, port), msgArr);
}
//실제로 동작시킬 메소드 Bootstrap 연결 옵션 설정 및 연결 처리
public void run() {
if(this.addr_ == null) {
logger.error("주소 정보가 없습니다.");
}else if(this.msgArr == null || this.msgArr.length == 0) {
logger.error("보낼 메시지가 없습니다.");
}
group = new NioEventLoopGroup(3);
bs.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.SO_KEEPALIVE, true);
doConnect();
}
private void doConnect() {
handlerSet();
ChannelFuture f = bs.connect(addr_);
channel_ = f.channel();
logger.info(addr_ + " connect()");
}
private void handlerSet() {
if(bs != null) {
bs.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
NettyClientHandler handler = new NettyClientHandler(msgArr[idx]);
handler.setCallBackClientHandler(new NettyClientAction() {
public void close(NettyClientHandler handler) {
//종료 처리 후 더 보낼게 존재한다면 기존 옵션으로 재 연결처리를 하는 콜백 메소드
closeAndContinue();
}
public void receive(NettyClientHandler handler) {
//응답 받은 메시지 콜백 메소드
String receiveMsg = handler.getReceiveMsg();
logger.info("callBack receive : "+ receiveMsg);
closeAndContinue();
}
});
ch.pipeline().addLast("clientHandler", handler);
}
});
}
}
private void closeAndContinue() {
try {
channel_.close().sync(); //현재의 채널을 일단 닫는다.
if(msgArr.length > ++idx) { //보낼 메시지가 남았으면 재연결 처리
doConnect();
}else { //보낼 메시지가 없다면 종료
bs.group().shutdownGracefully(); //eventLoop에 등록된 Thread를 종료 처리한다.
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 생성자를 통해 연결지의 IP, PORT정보를 얻고 보낼 메시지 배열을 전달받습니다.
- run() 메소드를 통해 전송을 위한 옵션 설정을 진행합니다. - Bootstrap 옵션 처리 (옵션처리는 최초에만 한번 설정)
- 옵션 설정이 끝나면 doConnect메소드를 통해 연결처리를 진행합니다.
- 연결 전 handler부분에서 메소드가 동작 후 콜백 받을 메소드를 정의합니다.
- 연결
- 동작들이 일어나고 receive 콜백 메소드가 동작하면 closeAndContinue() 메소드에서 더 보낼지 멈출지 판단합니다.
-> 더 이상 보낼 메시지가 없다면 설정한 그룹들의 EventGroup을 모두 종료하기 위해 shutdown처리를 합니다.
3. NettyClientHandler.java
package com.psw.socket.nettyPrj.netty.client;
import org.apache.log4j.Logger;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
public class NettyClientHandler extends ChannelInboundHandlerAdapter{
private static final Logger logger = Logger.getLogger(NettyClientHandler.class);
private String sendMsg;
public NettyClientHandler(String msg) {
this.sendMsg = msg;
}
private NettyClientAction action_;
public void setCallBackClientHandler(NettyClientAction action) {
this.action_ = action;
}
private String receiveMsg;
public String getReceiveMsg() {
return this.receiveMsg;
}
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
logger.info("채널 등록");
}
@Override
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
System.out.println("채널 연결이 종료됨.");
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
logger.info("채널이 메시지 발송할 준비가 됨.");
ByteBuf messageBuffer = Unpooled.buffer();
messageBuffer.writeBytes(sendMsg.getBytes());
ctx.writeAndFlush( messageBuffer ); //메시지를 발송하고 flush처리
logger.info("발송 메시지 >" + sendMsg);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
logger.info("메시지를 받는 메소드.");
ByteBuf buf = (ByteBuf)msg;
int n = buf.readableBytes();
if( n > 0 ) {
byte[] b = new byte[n];
buf.readBytes(b);
//수신메시지 출력
this.receiveMsg = new String( b );
logger.info("handler 수신된 메시지 >" + this.receiveMsg);
if(this.action_ != null) {
action_.receive(NettyClientHandler.this);
}
}
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
logger.info("메시지를 받는 동작이 끝나면 동작하는 메소드.");
}
}